crtx-reflect 0.1.1

Reflection orchestration, prompts, candidate parsing, and schema validation.
Documentation
use std::sync::atomic::{AtomicUsize, Ordering};

use async_trait::async_trait;
use cortex_core::MemoryId;
use cortex_llm::{blake3_hex, LlmAdapter, LlmError, LlmRequest, LlmResponse};
use cortex_reflect::{
    extract_candidates, AcceptedMemory, PrincipleExtractionWindow,
    DEFAULT_PRINCIPLE_EXTRACTION_MODEL,
};

#[derive(Debug)]
struct FixedPrincipleAdapter {
    output: String,
    calls: AtomicUsize,
}

impl FixedPrincipleAdapter {
    fn new(output: String) -> Self {
        Self {
            output,
            calls: AtomicUsize::new(0),
        }
    }

    fn calls(&self) -> usize {
        self.calls.load(Ordering::SeqCst)
    }
}

#[async_trait]
impl LlmAdapter for FixedPrincipleAdapter {
    fn adapter_id(&self) -> &'static str {
        "fixed-principles"
    }

    async fn complete(&self, req: LlmRequest) -> Result<LlmResponse, LlmError> {
        self.calls.fetch_add(1, Ordering::SeqCst);
        assert_eq!(req.model, DEFAULT_PRINCIPLE_EXTRACTION_MODEL);
        Ok(LlmResponse {
            text: self.output.clone(),
            parsed_json: None,
            model: req.model,
            usage: None,
            raw_hash: blake3_hex(self.output.as_bytes()),
        })
    }
}

fn mem(id: &str, claim: &str, domains: &[&str]) -> AcceptedMemory {
    AcceptedMemory {
        id: id.parse::<MemoryId>().expect("valid memory id"),
        claim: claim.to_string(),
        domains: domains.iter().map(|domain| (*domain).to_string()).collect(),
        applies_when: vec!["cross-domain behavior recurs".to_string()],
        does_not_apply_when: vec!["support is isolated".to_string()],
    }
}

#[tokio::test]
async fn principles_two_memories_in_one_domain_returns_empty_without_adapter_call() {
    let window = PrincipleExtractionWindow::new(vec![
        mem(
            "mem_01ARZ3NDEKTSV4RRFFQ69G5FAV",
            "Parser failures need typed evidence.",
            &["agents"],
        ),
        mem(
            "mem_01ARZ3NDEKTSV4RRFFQ69G5FAW",
            "Reflection output should be validated before use.",
            &["agents"],
        ),
    ]);
    let adapter = FixedPrincipleAdapter::new(r#"{"candidate_principles":[]}"#.to_string());

    let candidates = extract_candidates(window, &adapter)
        .await
        .expect("threshold miss is not an error");

    assert!(candidates.is_empty());
    assert_eq!(adapter.calls(), 0);
}

#[tokio::test]
async fn principles_three_memories_across_two_domains_parse_candidate_output() {
    let ids = [
        "mem_01ARZ3NDEKTSV4RRFFQ69G5FAV",
        "mem_01ARZ3NDEKTSV4RRFFQ69G5FAW",
        "mem_01ARZ3NDEKTSV4RRFFQ69G5FAX",
    ];
    let window = PrincipleExtractionWindow::new(vec![
        mem(
            ids[0],
            "Evidence must be inspected before claiming state.",
            &["agents"],
        ),
        mem(
            ids[1],
            "Audit reports need direct observed anchors.",
            &["audit"],
        ),
        mem(
            ids[2],
            "Candidate output must stay separate from promotion.",
            &["agents"],
        ),
    ]);
    let output = format!(
        r#"{{
          "candidate_principles": [
            {{
              "statement": "Only promote durable guidance after evidence-bound validation.",
              "supporting_memory_ids": ["{id0}", "{id1}", "{id2}"],
              "contradicting_memory_ids": [],
              "domains_observed": ["agents", "audit"],
              "applies_when": ["turning repeated memories into reusable guidance"],
              "does_not_apply_when": ["fewer than three accepted memories support it"],
              "alternative_interpretations": ["This may be a local workflow convention."],
              "confidence": 0.72,
              "overgeneralisation_risk": 0.24
            }}
          ]
        }}"#,
        id0 = ids[0],
        id1 = ids[1],
        id2 = ids[2],
    );
    let adapter = FixedPrincipleAdapter::new(output);

    let candidates = extract_candidates(window, &adapter)
        .await
        .expect("valid principle candidate batch parses");

    assert_eq!(adapter.calls(), 1);
    assert_eq!(candidates.len(), 1);
    assert_eq!(
        candidates[0].statement,
        "Only promote durable guidance after evidence-bound validation."
    );
    assert_eq!(candidates[0].supporting_memory_ids.len(), 3);
}

#[tokio::test]
async fn principles_adapter_output_below_support_threshold_is_filtered() {
    let ids = [
        "mem_01ARZ3NDEKTSV4RRFFQ69G5FAV",
        "mem_01ARZ3NDEKTSV4RRFFQ69G5FAW",
        "mem_01ARZ3NDEKTSV4RRFFQ69G5FAX",
    ];
    let window = PrincipleExtractionWindow::new(vec![
        mem(ids[0], "First accepted memory.", &["agents"]),
        mem(ids[1], "Second accepted memory.", &["audit"]),
        mem(ids[2], "Third accepted memory.", &["security"]),
    ]);
    let output = format!(
        r#"{{
          "candidate_principles": [
            {{
              "statement": "Unsupported proposal should be rejected.",
              "supporting_memory_ids": ["{id0}", "{id1}"],
              "contradicting_memory_ids": [],
              "domains_observed": ["agents", "audit"],
              "applies_when": ["model under-cites support"],
              "does_not_apply_when": ["thresholds are satisfied"],
              "alternative_interpretations": [],
              "confidence": 0.8,
              "overgeneralisation_risk": 0.1
            }}
          ]
        }}"#,
        id0 = ids[0],
        id1 = ids[1],
    );
    let adapter = FixedPrincipleAdapter::new(output);

    let candidates = extract_candidates(window, &adapter)
        .await
        .expect("valid batch parses before deterministic filtering");

    assert!(candidates.is_empty());
}