use std::sync::Arc;
use exocortex_kernel::{MemoryId, Provenance, Relationship, RelationshipId, Visibility};
use exocortex_storage::Storage;
use tokio::sync::mpsc;
use tracing::{instrument, warn};
use crate::rules::{self, Edge, EntityFact, TagFact};
pub enum ReasoningWork {
KHopOver {
seed: MemoryId,
k: u8,
},
SessionWrapup {
memories: Vec<MemoryId>,
},
}
pub struct ReasoningEngine<S: Storage> {
storage: Arc<S>,
tx_work: mpsc::Sender<ReasoningWork>,
rx_work: tokio::sync::Mutex<mpsc::Receiver<ReasoningWork>>,
k_hop: u8,
}
impl<S: Storage + 'static> ReasoningEngine<S> {
pub fn new(storage: Arc<S>, queue_depth: usize, k_hop: u8) -> Self {
rules::prime(&storage_ontology(&storage));
let (tx, rx) = mpsc::channel(queue_depth);
Self {
storage,
tx_work: tx,
rx_work: tokio::sync::Mutex::new(rx),
k_hop,
}
}
pub async fn enqueue(&self, w: ReasoningWork) {
if self.tx_work.try_send(w).is_err() {
metrics::counter!("exocortex_reasoning_dropped_total").increment(1);
warn!("reasoning queue full; dropping work");
}
}
pub async fn run(self: Arc<Self>) {
loop {
let w = { self.rx_work.lock().await.recv().await };
let Some(w) = w else { break };
match w {
ReasoningWork::KHopOver { seed, k } => self.k_hop_reason(seed, k).await,
ReasoningWork::SessionWrapup { memories } => self.session_reason(&memories).await,
}
}
}
#[instrument(skip(self))]
pub async fn k_hop_reason(&self, seed: MemoryId, k: u8) {
let k = k.clamp(1, self.k_hop.max(1));
let mut edges: Vec<Edge> = Vec::new();
let mut entities: Vec<EntityFact> = Vec::new();
let mut tags: Vec<TagFact> = Vec::new();
let mut evidence: Vec<RelationshipId> = Vec::new();
let mut adjacency: std::collections::HashMap<MemoryId, Vec<(MemoryId, RelationshipId)>> =
std::collections::HashMap::new();
{
use futures::StreamExt;
let mut rels = self.storage.stream_all_relationships().await;
while let Some(Ok(r)) = rels.next().await {
adjacency.entry(r.from).or_default().push((r.to, r.id));
adjacency.entry(r.to).or_default().push((r.from, r.id));
}
}
const MAX_NODES: usize = 512;
let mut neighborhood: std::collections::HashSet<MemoryId> =
std::collections::HashSet::from([seed]);
let mut seen_edges: std::collections::HashSet<RelationshipId> =
std::collections::HashSet::new();
let mut frontier = vec![seed];
for _hop in 0..k {
let mut next = Vec::new();
for node in &frontier {
if let Some(neighbors) = adjacency.get(node) {
for (other, edge_id) in neighbors {
if neighborhood.insert(*other) && neighborhood.len() <= MAX_NODES {
next.push(*other);
}
if seen_edges.insert(*edge_id) {
evidence.push(*edge_id);
}
}
}
}
if next.is_empty() || neighborhood.len() > MAX_NODES {
break;
}
frontier = next;
}
{
use futures::StreamExt;
let mut rels = self.storage.stream_all_relationships().await;
while let Some(Ok(r)) = rels.next().await {
if neighborhood.contains(&r.from) && neighborhood.contains(&r.to) {
edges.push(Edge(r.from, r.to, r.kind));
}
}
}
use futures::StreamExt;
let mut mems = self.storage.stream_all_memories().await;
while let Some(Ok(m)) = mems.next().await {
for t in &m.tags {
tags.push(TagFact(m.id, fxhash_tag(t.as_str())));
}
for e in &m.context.entities {
entities.push(EntityFact(m.id, *e));
}
}
drop(mems);
let derived = rules::evaluate(edges, entities, tags);
self.write_back(derived, &evidence).await;
}
async fn session_reason(&self, ms: &[MemoryId]) {
for m in ms {
self.k_hop_reason(*m, 3).await;
}
}
async fn write_back(&self, derived: rules::Derived, evidence: &[RelationshipId]) {
let ontology = self.storage_ontology();
let mut new_rels: Vec<Relationship> = Vec::new();
let now = chrono::Utc::now();
let mut push = |from: MemoryId, to: MemoryId, rule_id: &str, strength: f32| {
let kind = derived_kind(&ontology, rule_id);
let id = RelationshipId::derive(from, kind, to, None);
new_rels.push(Relationship {
id,
kind,
from,
to,
visibility: Visibility::Org,
provenance: Provenance::Derived {
rule_id: rule_id.into(),
evidence: evidence.to_vec(),
},
properties: exocortex_kernel::RelationshipProperties {
strength,
confidence: derived_confidence(rule_id, evidence.len()),
context: None,
evidence_count: 1,
success_rate: None,
validation_count: 0,
counter_evidence_count: 0,
last_validated: now,
},
description: None,
bidirectional: false,
valid_from: now,
valid_until: None,
recorded_at: now,
invalidated_by: None,
lsn: exocortex_kernel::LSN::new_local(0),
});
};
for (a, b) in derived.transitive_depends_on {
push(a, b, "R4", 0.5);
}
for (a, b) in derived.transitive_requires {
push(a, b, "R5", 0.5);
}
for (a, b) in derived.co_occurrence_affinity {
push(a, b, "R7", 0.3);
}
for (a, b) in derived.problem_solution_bridge {
push(a, b, "R8", 0.3);
}
for (a, b) in derived.similar_tags_affinity {
push(a, b, "R9", 0.3);
}
for (a, b) in derived.implied_solves {
push(a, b, "D1", 0.8);
}
for (a, b) in derived.transitive_builds_on {
push(a, b, "D2", 0.5);
}
for (a, b) in derived.indirect_blocker {
push(a, b, "D3", 0.5);
}
for (a, b) in derived.session_cohort {
push(a, b, "D6", 0.6);
}
if new_rels.is_empty() {
return;
}
use futures::StreamExt;
let existing: std::collections::HashSet<RelationshipId> = {
let mut set = std::collections::HashSet::new();
let mut rs = self.storage.stream_all_relationships().await;
while let Some(Ok(r)) = rs.next().await {
set.insert(r.id);
}
set
};
let fresh: Vec<Relationship> = new_rels
.into_iter()
.filter(|r| !existing.contains(&r.id))
.collect();
if !fresh.is_empty() {
if let Err(e) = self.storage.upsert_batch(&[], &fresh).await {
warn!(?e, "derived-edge writeback failed");
} else {
metrics::counter!("exocortex_rules_executed_total", "engine" => "crepe")
.increment(fresh.len() as u64);
}
}
}
pub async fn inferred_type(&self, id: MemoryId) -> Option<u8> {
let mut edges = Vec::new();
use futures::StreamExt;
let mut rels = self.storage.stream_all_relationships().await;
while let Some(Ok(r)) = rels.next().await {
if r.from == id || r.to == id {
edges.push(Edge(r.from, r.to, r.kind));
}
}
drop(rels);
let derived = rules::evaluate(edges, vec![], vec![]);
derived
.type_from_solves
.iter()
.chain(derived.type_from_fixes.iter())
.chain(derived.type_from_causes.iter())
.find(|(m, _)| *m == id)
.map(|(_, t)| *t)
}
fn storage_ontology(&self) -> exocortex_kernel::Ontology {
storage_ontology(&self.storage)
}
}
fn storage_ontology<S: Storage>(_: &Arc<S>) -> exocortex_kernel::Ontology {
exocortex_kernel::Ontology::from_packs(vec![exocortex_pack_dev_v1::pack_def()])
.expect("linked pack assembles")
}
fn derived_kind(onto: &exocortex_kernel::Ontology, rule_id: &str) -> exocortex_kernel::RelKindId {
match rule_id {
"R4" => onto.kind_id("DependsOn").expect("kind"),
"R5" => onto.kind_id("Requires").expect("kind"),
"D1" => exocortex_kernel::kinds::SOLVES,
"D2" => onto.kind_id("BuildsOn").expect("kind"),
"D3" => onto.kind_id("Blocks").expect("kind"),
"D6" => exocortex_kernel::kinds::IN_SESSION,
_ => onto.kind_id("RelatedTo").expect("kind"),
}
}
fn derived_confidence(rule_id: &str, evidence: usize) -> f32 {
match rule_id {
"R4" | "R5" => 1.0 / 3.0, "R7" => (evidence as f32 / 5.0).min(1.0),
"R9" => (evidence as f32 / 5.0).min(1.0),
"R8" | "D1" => 0.8,
_ => 0.5,
}
}
fn fxhash_tag(tag: &str) -> u32 {
let mut h: u32 = 0x811c_9dc5;
for b in tag.as_bytes() {
h ^= *b as u32;
h = h.wrapping_mul(0x0100_0193);
}
h
}