use std::collections::HashMap;
use std::fmt;
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct NodeId(u32);
impl NodeId {
const MAX_SEQ: u32 = 0x0FFF_FFFF;
pub fn new(kind: NodeKind, seq: u32) -> Self {
assert!(seq <= Self::MAX_SEQ, "NodeId sequence overflow: {seq}");
let tag = kind.discriminant() as u32;
debug_assert!(tag < 16);
Self((tag << 28) | seq)
}
#[inline]
pub fn kind(self) -> NodeKind {
NodeKind::from_discriminant((self.0 >> 28) as u8)
}
#[inline]
pub fn seq(self) -> u32 {
self.0 & Self::MAX_SEQ
}
#[inline]
pub fn to_raw(self) -> u32 {
self.0
}
#[inline]
pub fn from_raw(raw: u32) -> Self {
Self(raw)
}
#[inline]
pub fn is_nil(self) -> bool {
self.0 == 0
}
pub const NIL: NodeId = NodeId(0);
}
impl fmt::Debug for NodeId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "NodeId({:?}:{})", self.kind(), self.seq())
}
}
impl fmt::Display for NodeId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.kind().as_str(), self.seq())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum NodeKind {
Entity = 0,
Episode = 1,
Belief = 2,
Goal = 3,
Task = 4,
IntentHypothesis = 5,
Routine = 6,
Need = 7,
Opportunity = 8,
Risk = 9,
Constraint = 10,
Preference = 11,
ConversationThread = 12,
ActionSchema = 13,
}
impl NodeKind {
pub const COUNT: usize = 14;
#[inline]
pub fn discriminant(self) -> u8 {
self as u8
}
#[inline]
pub fn from_discriminant(d: u8) -> Self {
match d {
0 => Self::Entity,
1 => Self::Episode,
2 => Self::Belief,
3 => Self::Goal,
4 => Self::Task,
5 => Self::IntentHypothesis,
6 => Self::Routine,
7 => Self::Need,
8 => Self::Opportunity,
9 => Self::Risk,
10 => Self::Constraint,
11 => Self::Preference,
12 => Self::ConversationThread,
13 => Self::ActionSchema,
_ => Self::Entity,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Entity => "entity",
Self::Episode => "episode",
Self::Belief => "belief",
Self::Goal => "goal",
Self::Task => "task",
Self::IntentHypothesis => "intent_hypothesis",
Self::Routine => "routine",
Self::Need => "need",
Self::Opportunity => "opportunity",
Self::Risk => "risk",
Self::Constraint => "constraint",
Self::Preference => "preference",
Self::ConversationThread => "conversation_thread",
Self::ActionSchema => "action_schema",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"entity" => Some(Self::Entity),
"episode" => Some(Self::Episode),
"belief" => Some(Self::Belief),
"goal" => Some(Self::Goal),
"task" => Some(Self::Task),
"intent_hypothesis" => Some(Self::IntentHypothesis),
"routine" => Some(Self::Routine),
"need" => Some(Self::Need),
"opportunity" => Some(Self::Opportunity),
"risk" => Some(Self::Risk),
"constraint" => Some(Self::Constraint),
"preference" => Some(Self::Preference),
"conversation_thread" => Some(Self::ConversationThread),
"action_schema" => Some(Self::ActionSchema),
_ => None,
}
}
pub fn is_persistent(self) -> bool {
!matches!(self, Self::IntentHypothesis | Self::ConversationThread)
}
pub fn supports_belief_revision(self) -> bool {
matches!(self, Self::Belief | Self::Preference)
}
pub fn is_time_sensitive(self) -> bool {
matches!(
self,
Self::Task | Self::Opportunity | Self::Need | Self::Risk
)
}
pub const ALL: [NodeKind; Self::COUNT] = [
Self::Entity,
Self::Episode,
Self::Belief,
Self::Goal,
Self::Task,
Self::IntentHypothesis,
Self::Routine,
Self::Need,
Self::Opportunity,
Self::Risk,
Self::Constraint,
Self::Preference,
Self::ConversationThread,
Self::ActionSchema,
];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Provenance {
Observed,
Inferred,
Told,
Experimented,
Extracted,
Consolidated,
SystemDefault,
}
impl Provenance {
pub fn as_str(self) -> &'static str {
match self {
Self::Observed => "observed",
Self::Inferred => "inferred",
Self::Told => "told",
Self::Experimented => "experimented",
Self::Extracted => "extracted",
Self::Consolidated => "consolidated",
Self::SystemDefault => "system_default",
}
}
pub fn from_str(s: &str) -> Self {
match s {
"observed" => Self::Observed,
"inferred" => Self::Inferred,
"told" => Self::Told,
"experimented" => Self::Experimented,
"extracted" => Self::Extracted,
"consolidated" => Self::Consolidated,
"system_default" => Self::SystemDefault,
_ => Self::Observed,
}
}
pub fn reliability_prior(self) -> f64 {
match self {
Self::Told => 0.95, Self::Observed => 0.90, Self::Experimented => 0.85, Self::Extracted => 0.75, Self::Inferred => 0.60, Self::Consolidated => 0.80, Self::SystemDefault => 0.50, }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CognitiveAttrs {
pub confidence: f64,
pub activation: f64,
pub salience: f64,
pub persistence: f64,
pub valence: f64,
pub urgency: f64,
pub novelty: f64,
pub last_updated_ms: u64,
pub volatility: f64,
pub provenance: Provenance,
pub evidence_count: u32,
}
impl CognitiveAttrs {
pub fn default_for(kind: NodeKind) -> Self {
let (confidence, salience, persistence) = match kind {
NodeKind::Entity => (0.80, 0.50, 0.90),
NodeKind::Episode => (0.95, 0.40, 0.30),
NodeKind::Belief => (0.50, 0.60, 0.80),
NodeKind::Goal => (0.70, 0.80, 0.85),
NodeKind::Task => (0.80, 0.70, 0.50),
NodeKind::IntentHypothesis => (0.30, 0.90, 0.05),
NodeKind::Routine => (0.50, 0.50, 0.70),
NodeKind::Need => (0.60, 0.70, 0.40),
NodeKind::Opportunity => (0.40, 0.60, 0.20),
NodeKind::Risk => (0.40, 0.70, 0.60),
NodeKind::Constraint => (0.90, 0.80, 0.95),
NodeKind::Preference => (0.60, 0.50, 0.85),
NodeKind::ConversationThread => (0.90, 0.80, 0.10),
NodeKind::ActionSchema => (0.70, 0.40, 0.90),
};
Self {
confidence,
activation: 0.0,
salience,
persistence,
valence: 0.0,
urgency: 0.0,
novelty: 1.0,
last_updated_ms: now_ms(),
volatility: 0.1,
provenance: Provenance::Observed,
evidence_count: 1,
}
}
pub fn clamp(&mut self) {
self.confidence = self.confidence.clamp(0.0, 1.0);
self.activation = self.activation.clamp(0.0, 1.0);
self.salience = self.salience.clamp(0.0, 1.0);
self.persistence = self.persistence.clamp(0.0, 1.0);
self.valence = self.valence.clamp(-1.0, 1.0);
self.urgency = self.urgency.clamp(0.0, 1.0);
self.novelty = self.novelty.clamp(0.0, 1.0);
self.volatility = self.volatility.clamp(0.0, 1.0);
}
pub fn age_secs(&self) -> f64 {
let now = now_ms();
if now > self.last_updated_ms {
(now - self.last_updated_ms) as f64 / 1000.0
} else {
0.0
}
}
pub fn relevance_score(&self) -> f64 {
let recency = (-self.age_secs() / (3600.0 * 24.0)).exp(); let novelty_boost = 1.0 + 0.2 * self.novelty;
(0.35 * self.activation
+ 0.25 * self.salience
+ 0.15 * self.persistence
+ 0.15 * self.urgency
+ 0.10 * recency)
* novelty_boost
}
pub fn touch(&mut self, activation_boost: f64) {
self.last_updated_ms = now_ms();
self.activation = (self.activation + activation_boost).min(1.0);
}
pub fn decay(&mut self, elapsed_secs: f64) {
let activation_half_life = 300.0 + 3300.0 * self.persistence; self.activation *= f64::powf(2.0, -elapsed_secs / activation_half_life);
self.novelty *= f64::powf(2.0, -elapsed_secs / 1800.0);
if self.activation < 1e-6 {
self.activation = 0.0;
}
if self.novelty < 1e-6 {
self.novelty = 0.0;
}
}
}
impl Default for CognitiveAttrs {
fn default() -> Self {
Self::default_for(NodeKind::Entity)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum NodePayload {
Entity(EntityPayload),
Episode(EpisodePayload),
Belief(BeliefPayload),
Goal(GoalPayload),
Task(TaskPayload),
IntentHypothesis(IntentPayload),
Routine(RoutinePayload),
Need(NeedPayload),
Opportunity(OpportunityPayload),
Risk(RiskPayload),
Constraint(ConstraintPayload),
Preference(PreferencePayload),
ConversationThread(ConversationPayload),
ActionSchema(ActionSchemaPayload),
}
impl NodePayload {
pub fn kind(&self) -> NodeKind {
match self {
Self::Entity(_) => NodeKind::Entity,
Self::Episode(_) => NodeKind::Episode,
Self::Belief(_) => NodeKind::Belief,
Self::Goal(_) => NodeKind::Goal,
Self::Task(_) => NodeKind::Task,
Self::IntentHypothesis(_) => NodeKind::IntentHypothesis,
Self::Routine(_) => NodeKind::Routine,
Self::Need(_) => NodeKind::Need,
Self::Opportunity(_) => NodeKind::Opportunity,
Self::Risk(_) => NodeKind::Risk,
Self::Constraint(_) => NodeKind::Constraint,
Self::Preference(_) => NodeKind::Preference,
Self::ConversationThread(_) => NodeKind::ConversationThread,
Self::ActionSchema(_) => NodeKind::ActionSchema,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityPayload {
pub name: String,
pub entity_type: String,
pub memory_rids: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EpisodePayload {
pub memory_rid: String,
pub summary: String,
pub occurred_at: f64,
pub participants: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BeliefPayload {
pub proposition: String,
pub log_odds: f64,
pub domain: String,
pub evidence_trail: Vec<EvidenceEntry>,
pub user_confirmed: bool,
}
impl BeliefPayload {
pub fn probability(&self) -> f64 {
sigmoid(self.log_odds)
}
pub fn update(&mut self, weight: f64, reliability: f64, source: &str, timestamp: f64) {
let effective_weight = weight * reliability;
self.log_odds += effective_weight;
self.evidence_trail.push(EvidenceEntry {
source: source.to_string(),
weight: effective_weight,
timestamp,
});
}
pub fn support_strength(&self) -> f64 {
self.evidence_trail
.iter()
.filter(|e| e.weight > 0.0)
.map(|e| e.weight)
.sum()
}
pub fn contradiction_strength(&self) -> f64 {
self.evidence_trail
.iter()
.filter(|e| e.weight < 0.0)
.map(|e| e.weight.abs())
.sum()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidenceEntry {
pub source: String,
pub weight: f64,
pub timestamp: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GoalPayload {
pub description: String,
pub status: GoalStatus,
pub progress: f64,
pub deadline: Option<f64>,
pub priority: Priority,
pub parent_goal: Option<NodeId>,
pub completion_criteria: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum GoalStatus {
Active,
Paused,
Completed,
Abandoned,
Blocked,
}
impl GoalStatus {
pub fn as_str(self) -> &'static str {
match self {
Self::Active => "active",
Self::Paused => "paused",
Self::Completed => "completed",
Self::Abandoned => "abandoned",
Self::Blocked => "blocked",
}
}
pub fn from_str(s: &str) -> Self {
match s {
"active" => Self::Active,
"paused" => Self::Paused,
"completed" => Self::Completed,
"abandoned" => Self::Abandoned,
"blocked" => Self::Blocked,
_ => Self::Active,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Priority {
Low = 0,
Medium = 1,
High = 2,
Critical = 3,
}
impl Priority {
pub fn as_str(self) -> &'static str {
match self {
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
Self::Critical => "critical",
}
}
pub fn from_str(s: &str) -> Self {
match s {
"low" => Self::Low,
"medium" => Self::Medium,
"high" => Self::High,
"critical" => Self::Critical,
_ => Self::Medium,
}
}
pub fn weight(self) -> f64 {
match self {
Self::Low => 0.25,
Self::Medium => 0.50,
Self::High => 0.75,
Self::Critical => 1.00,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskPayload {
pub description: String,
pub status: TaskStatus,
pub goal_id: Option<NodeId>,
pub deadline: Option<f64>,
pub priority: Priority,
pub estimated_minutes: Option<u32>,
pub prerequisites: Vec<NodeId>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TaskStatus {
Pending,
InProgress,
Completed,
Cancelled,
Blocked,
}
impl TaskStatus {
pub fn as_str(self) -> &'static str {
match self {
Self::Pending => "pending",
Self::InProgress => "in_progress",
Self::Completed => "completed",
Self::Cancelled => "cancelled",
Self::Blocked => "blocked",
}
}
pub fn from_str(s: &str) -> Self {
match s {
"pending" => Self::Pending,
"in_progress" => Self::InProgress,
"completed" => Self::Completed,
"cancelled" => Self::Cancelled,
"blocked" => Self::Blocked,
_ => Self::Pending,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IntentPayload {
pub description: String,
pub features: Vec<f64>,
pub posterior: f64,
pub candidate_actions: Vec<NodeId>,
pub source_context: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoutinePayload {
pub description: String,
pub period_secs: f64,
pub phase_offset_secs: f64,
pub reliability: f64,
pub observation_count: u32,
pub last_triggered: f64,
pub action_description: String,
pub weekday_mask: u8,
}
impl RoutinePayload {
pub fn next_occurrence(&self, now: f64) -> f64 {
if self.period_secs <= 0.0 {
return f64::INFINITY;
}
let cycles_since_phase = ((now - self.phase_offset_secs) / self.period_secs).floor();
let next = self.phase_offset_secs + (cycles_since_phase + 1.0) * self.period_secs;
if next <= now {
next + self.period_secs
} else {
next
}
}
pub fn time_until_next(&self, now: f64) -> f64 {
self.next_occurrence(now) - now
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NeedPayload {
pub description: String,
pub category: NeedCategory,
pub intensity: f64,
pub last_satisfied: Option<f64>,
pub satisfaction_pattern: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum NeedCategory {
Informational,
Social,
Emotional,
Organizational,
Creative,
Health,
Financial,
Professional,
}
impl NeedCategory {
pub fn as_str(self) -> &'static str {
match self {
Self::Informational => "informational",
Self::Social => "social",
Self::Emotional => "emotional",
Self::Organizational => "organizational",
Self::Creative => "creative",
Self::Health => "health",
Self::Financial => "financial",
Self::Professional => "professional",
}
}
pub fn from_str(s: &str) -> Self {
match s {
"informational" => Self::Informational,
"social" => Self::Social,
"emotional" => Self::Emotional,
"organizational" => Self::Organizational,
"creative" => Self::Creative,
"health" => Self::Health,
"financial" => Self::Financial,
"professional" => Self::Professional,
_ => Self::Informational,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpportunityPayload {
pub description: String,
pub expires_at: f64,
pub expected_benefit: f64,
pub required_action: String,
pub relevant_goals: Vec<NodeId>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RiskPayload {
pub description: String,
pub severity: f64,
pub likelihood: f64,
pub mitigation: String,
pub threatened_goals: Vec<NodeId>,
}
impl RiskPayload {
pub fn expected_impact(&self) -> f64 {
self.severity * self.likelihood
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConstraintPayload {
pub description: String,
pub constraint_type: ConstraintType,
pub condition: String,
pub imposed_by: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConstraintType {
Hard,
Soft,
}
impl ConstraintType {
pub fn as_str(self) -> &'static str {
match self {
Self::Hard => "hard",
Self::Soft => "soft",
}
}
pub fn from_str(s: &str) -> Self {
match s {
"hard" => Self::Hard,
"soft" => Self::Soft,
_ => Self::Soft,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreferencePayload {
pub domain: String,
pub preferred: String,
pub dispreferred: Option<String>,
pub strength: f64,
pub log_odds: f64,
pub observation_count: u32,
}
impl PreferencePayload {
pub fn probability(&self) -> f64 {
sigmoid(self.log_odds)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConversationPayload {
pub topic: String,
pub valence_history: Vec<f64>,
pub open_items: Vec<String>,
pub turn_count: u32,
pub started_at: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActionSchemaPayload {
pub name: String,
pub description: String,
pub action_kind: ActionKind,
pub preconditions: Vec<Precondition>,
pub effects: Vec<Effect>,
pub confidence_threshold: f64,
pub success_rate: f64,
pub execution_count: u32,
pub acceptance_count: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ActionKind {
Communicate,
Inform,
Organize,
Schedule,
Suggest,
Warn,
Execute,
Abstain,
}
impl ActionKind {
pub fn as_str(self) -> &'static str {
match self {
Self::Communicate => "communicate",
Self::Inform => "inform",
Self::Organize => "organize",
Self::Schedule => "schedule",
Self::Suggest => "suggest",
Self::Warn => "warn",
Self::Execute => "execute",
Self::Abstain => "abstain",
}
}
pub fn from_str(s: &str) -> Self {
match s {
"communicate" => Self::Communicate,
"inform" => Self::Inform,
"organize" => Self::Organize,
"schedule" => Self::Schedule,
"suggest" => Self::Suggest,
"warn" => Self::Warn,
"execute" => Self::Execute,
"abstain" => Self::Abstain,
_ => Self::Abstain,
}
}
pub fn base_cost(self) -> f64 {
match self {
Self::Abstain => 0.0,
Self::Inform => 0.05,
Self::Organize => 0.10,
Self::Suggest => 0.15,
Self::Communicate => 0.20,
Self::Schedule => 0.25,
Self::Warn => 0.30,
Self::Execute => 0.40,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Precondition {
pub description: String,
pub node_ref: Option<NodeId>,
pub required: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Effect {
pub description: String,
pub probability: f64,
pub utility: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CognitiveNode {
pub id: NodeId,
pub attrs: CognitiveAttrs,
pub payload: NodePayload,
pub label: String,
pub metadata: HashMap<String, serde_json::Value>,
}
impl CognitiveNode {
pub fn new(id: NodeId, label: String, payload: NodePayload) -> Self {
let attrs = CognitiveAttrs::default_for(id.kind());
Self {
id,
attrs,
payload,
label,
metadata: HashMap::new(),
}
}
pub fn with_attrs(id: NodeId, label: String, payload: NodePayload, attrs: CognitiveAttrs) -> Self {
Self {
id,
attrs,
payload,
label,
metadata: HashMap::new(),
}
}
#[inline]
pub fn kind(&self) -> NodeKind {
self.id.kind()
}
pub fn is_persistent(&self) -> bool {
self.id.kind().is_persistent()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CognitiveEdgeKind {
Supports,
Contradicts,
Causes,
Predicts,
Prevents,
AdvancesGoal,
BlocksGoal,
SubtaskOf,
Requires,
AssociatedWith,
InstanceOf,
PartOf,
SimilarTo,
PrecedesTemporally,
Triggers,
Prefers,
Avoids,
Constrains,
}
impl CognitiveEdgeKind {
pub const COUNT: usize = 18;
pub fn as_str(self) -> &'static str {
match self {
Self::Supports => "supports",
Self::Contradicts => "contradicts",
Self::Causes => "causes",
Self::Predicts => "predicts",
Self::Prevents => "prevents",
Self::AdvancesGoal => "advances_goal",
Self::BlocksGoal => "blocks_goal",
Self::SubtaskOf => "subtask_of",
Self::Requires => "requires",
Self::AssociatedWith => "associated_with",
Self::InstanceOf => "instance_of",
Self::PartOf => "part_of",
Self::SimilarTo => "similar_to",
Self::PrecedesTemporally => "precedes_temporally",
Self::Triggers => "triggers",
Self::Prefers => "prefers",
Self::Avoids => "avoids",
Self::Constrains => "constrains",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"supports" => Some(Self::Supports),
"contradicts" => Some(Self::Contradicts),
"causes" => Some(Self::Causes),
"predicts" => Some(Self::Predicts),
"prevents" => Some(Self::Prevents),
"advances_goal" => Some(Self::AdvancesGoal),
"blocks_goal" => Some(Self::BlocksGoal),
"subtask_of" => Some(Self::SubtaskOf),
"requires" => Some(Self::Requires),
"associated_with" => Some(Self::AssociatedWith),
"instance_of" => Some(Self::InstanceOf),
"part_of" => Some(Self::PartOf),
"similar_to" => Some(Self::SimilarTo),
"precedes_temporally" => Some(Self::PrecedesTemporally),
"triggers" => Some(Self::Triggers),
"prefers" => Some(Self::Prefers),
"avoids" => Some(Self::Avoids),
"constrains" => Some(Self::Constrains),
_ => None,
}
}
pub fn activation_transfer(self) -> f64 {
match self {
Self::Supports => 0.7,
Self::Causes => 0.8,
Self::AdvancesGoal => 0.6,
Self::Triggers => 0.7,
Self::Requires => 0.5,
Self::SubtaskOf => 0.4,
Self::Predicts => 0.4,
Self::AssociatedWith => 0.3,
Self::SimilarTo => 0.3,
Self::InstanceOf => 0.3,
Self::PartOf => 0.3,
Self::Prefers => 0.3,
Self::PrecedesTemporally => 0.2,
Self::Contradicts => -0.5,
Self::Prevents => -0.6,
Self::BlocksGoal => -0.5,
Self::Avoids => -0.3,
Self::Constrains => -0.2,
}
}
pub fn is_inhibitory(self) -> bool {
self.activation_transfer() < 0.0
}
pub fn is_epistemic(self) -> bool {
matches!(self, Self::Supports | Self::Contradicts)
}
pub fn is_causal(self) -> bool {
matches!(self, Self::Causes | Self::Predicts | Self::Prevents)
}
pub const ALL: [CognitiveEdgeKind; Self::COUNT] = [
Self::Supports,
Self::Contradicts,
Self::Causes,
Self::Predicts,
Self::Prevents,
Self::AdvancesGoal,
Self::BlocksGoal,
Self::SubtaskOf,
Self::Requires,
Self::AssociatedWith,
Self::InstanceOf,
Self::PartOf,
Self::SimilarTo,
Self::PrecedesTemporally,
Self::Triggers,
Self::Prefers,
Self::Avoids,
Self::Constrains,
];
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CognitiveEdge {
pub src: NodeId,
pub dst: NodeId,
pub kind: CognitiveEdgeKind,
pub weight: f64,
pub created_at_ms: u64,
pub last_confirmed_ms: u64,
pub observation_count: u32,
pub confidence: f64,
}
impl CognitiveEdge {
pub fn new(src: NodeId, dst: NodeId, kind: CognitiveEdgeKind, weight: f64) -> Self {
let now = now_ms();
Self {
src,
dst,
kind,
weight: weight.clamp(-1.0, 1.0),
created_at_ms: now,
last_confirmed_ms: now,
observation_count: 1,
confidence: 0.5,
}
}
pub fn effective_activation_transfer(&self) -> f64 {
self.weight * self.confidence * self.kind.activation_transfer()
}
pub fn confirm(&mut self) {
self.last_confirmed_ms = now_ms();
self.observation_count += 1;
self.confidence = 1.0 - (1.0 - self.confidence) * 0.85;
}
}
#[derive(Debug, Clone)]
pub struct NodeIdAllocator {
next_seq: [u32; NodeKind::COUNT],
}
impl NodeIdAllocator {
pub fn new() -> Self {
Self {
next_seq: [1; NodeKind::COUNT],
}
}
pub fn from_high_water_marks(marks: &[(NodeKind, u32)]) -> Self {
let mut alloc = Self::new();
for &(kind, hwm) in marks {
alloc.next_seq[kind.discriminant() as usize] = hwm + 1;
}
alloc
}
pub fn alloc(&mut self, kind: NodeKind) -> NodeId {
let idx = kind.discriminant() as usize;
let seq = self.next_seq[idx];
assert!(seq <= NodeId::MAX_SEQ, "NodeId sequence exhausted for {:?}", kind);
self.next_seq[idx] = seq + 1;
NodeId::new(kind, seq)
}
pub fn high_water_mark(&self, kind: NodeKind) -> u32 {
let idx = kind.discriminant() as usize;
if self.next_seq[idx] > 0 {
self.next_seq[idx] - 1
} else {
0
}
}
}
impl Default for NodeIdAllocator {
fn default() -> Self {
Self::new()
}
}
#[inline]
pub fn sigmoid(x: f64) -> f64 {
1.0 / (1.0 + (-x).exp())
}
#[inline]
pub fn logit(p: f64) -> f64 {
let p = p.clamp(1e-10, 1.0 - 1e-10);
(p / (1.0 - p)).ln()
}
pub fn now_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64
}
pub fn now_secs() -> f64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs_f64()
}
pub fn serialize_payload(payload: &NodePayload) -> serde_json::Value {
match payload {
NodePayload::Entity(p) => serde_json::to_value(p).unwrap_or_default(),
NodePayload::Episode(p) => serde_json::to_value(p).unwrap_or_default(),
NodePayload::Belief(p) => serde_json::to_value(p).unwrap_or_default(),
NodePayload::Goal(p) => serde_json::to_value(p).unwrap_or_default(),
NodePayload::Task(p) => serde_json::to_value(p).unwrap_or_default(),
NodePayload::IntentHypothesis(p) => serde_json::to_value(p).unwrap_or_default(),
NodePayload::Routine(p) => serde_json::to_value(p).unwrap_or_default(),
NodePayload::Need(p) => serde_json::to_value(p).unwrap_or_default(),
NodePayload::Opportunity(p) => serde_json::to_value(p).unwrap_or_default(),
NodePayload::Risk(p) => serde_json::to_value(p).unwrap_or_default(),
NodePayload::Constraint(p) => serde_json::to_value(p).unwrap_or_default(),
NodePayload::Preference(p) => serde_json::to_value(p).unwrap_or_default(),
NodePayload::ConversationThread(p) => serde_json::to_value(p).unwrap_or_default(),
NodePayload::ActionSchema(p) => serde_json::to_value(p).unwrap_or_default(),
}
}
pub fn deserialize_payload(kind: NodeKind, json: &serde_json::Value) -> Option<NodePayload> {
match kind {
NodeKind::Entity => serde_json::from_value(json.clone()).ok().map(NodePayload::Entity),
NodeKind::Episode => serde_json::from_value(json.clone()).ok().map(NodePayload::Episode),
NodeKind::Belief => serde_json::from_value(json.clone()).ok().map(NodePayload::Belief),
NodeKind::Goal => serde_json::from_value(json.clone()).ok().map(NodePayload::Goal),
NodeKind::Task => serde_json::from_value(json.clone()).ok().map(NodePayload::Task),
NodeKind::IntentHypothesis => serde_json::from_value(json.clone()).ok().map(NodePayload::IntentHypothesis),
NodeKind::Routine => serde_json::from_value(json.clone()).ok().map(NodePayload::Routine),
NodeKind::Need => serde_json::from_value(json.clone()).ok().map(NodePayload::Need),
NodeKind::Opportunity => serde_json::from_value(json.clone()).ok().map(NodePayload::Opportunity),
NodeKind::Risk => serde_json::from_value(json.clone()).ok().map(NodePayload::Risk),
NodeKind::Constraint => serde_json::from_value(json.clone()).ok().map(NodePayload::Constraint),
NodeKind::Preference => serde_json::from_value(json.clone()).ok().map(NodePayload::Preference),
NodeKind::ConversationThread => serde_json::from_value(json.clone()).ok().map(NodePayload::ConversationThread),
NodeKind::ActionSchema => serde_json::from_value(json.clone()).ok().map(NodePayload::ActionSchema),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_node_id_roundtrip() {
for kind in NodeKind::ALL {
let id = NodeId::new(kind, 42);
assert_eq!(id.kind(), kind);
assert_eq!(id.seq(), 42);
let raw = id.to_raw();
let restored = NodeId::from_raw(raw);
assert_eq!(restored.kind(), kind);
assert_eq!(restored.seq(), 42);
}
}
#[test]
fn test_node_id_nil() {
assert!(NodeId::NIL.is_nil());
assert_eq!(NodeId::NIL.kind(), NodeKind::Entity);
assert_eq!(NodeId::NIL.seq(), 0);
}
#[test]
fn test_node_id_max_seq() {
let id = NodeId::new(NodeKind::Belief, NodeId::MAX_SEQ);
assert_eq!(id.kind(), NodeKind::Belief);
assert_eq!(id.seq(), NodeId::MAX_SEQ);
}
#[test]
#[should_panic(expected = "NodeId sequence overflow")]
fn test_node_id_overflow() {
NodeId::new(NodeKind::Entity, NodeId::MAX_SEQ + 1);
}
#[test]
fn test_node_id_display() {
let id = NodeId::new(NodeKind::Goal, 7);
assert_eq!(format!("{id}"), "goal:7");
}
#[test]
fn test_node_kind_roundtrip() {
for kind in NodeKind::ALL {
let s = kind.as_str();
let parsed = NodeKind::from_str(s).unwrap();
assert_eq!(parsed, kind);
let d = kind.discriminant();
let from_d = NodeKind::from_discriminant(d);
assert_eq!(from_d, kind);
}
}
#[test]
fn test_provenance_reliability() {
assert!(Provenance::Told.reliability_prior() > Provenance::Observed.reliability_prior());
assert!(Provenance::Observed.reliability_prior() > Provenance::Inferred.reliability_prior());
assert!(Provenance::Inferred.reliability_prior() > Provenance::SystemDefault.reliability_prior());
}
#[test]
fn test_sigmoid_logit_roundtrip() {
for &p in &[0.01, 0.1, 0.25, 0.5, 0.75, 0.9, 0.99] {
let lo = logit(p);
let p2 = sigmoid(lo);
assert!((p - p2).abs() < 1e-10, "sigmoid(logit({p})) = {p2}");
}
}
#[test]
fn test_sigmoid_extremes() {
assert!((sigmoid(0.0) - 0.5).abs() < 1e-10);
assert!(sigmoid(10.0) > 0.999);
assert!(sigmoid(-10.0) < 0.001);
}
#[test]
fn test_belief_update() {
let mut belief = BeliefPayload {
proposition: "User prefers dark mode".to_string(),
log_odds: 0.0, domain: "preferences".to_string(),
evidence_trail: vec![],
user_confirmed: false,
};
assert!((belief.probability() - 0.5).abs() < 1e-10);
belief.update(1.5, 0.9, "observed dark mode setting", 1000.0);
assert!(belief.probability() > 0.7);
belief.update(-0.5, 0.8, "switched to light mode once", 2000.0);
let p = belief.probability();
assert!(p > 0.5 && p < 0.9);
assert_eq!(belief.evidence_trail.len(), 2);
assert!(belief.support_strength() > 0.0);
assert!(belief.contradiction_strength() > 0.0);
}
#[test]
fn test_cognitive_attrs_decay() {
let mut attrs = CognitiveAttrs {
activation: 1.0,
novelty: 1.0,
persistence: 0.5,
..CognitiveAttrs::default_for(NodeKind::Belief)
};
attrs.decay(600.0);
assert!(attrs.activation < 1.0, "activation should decay");
assert!(attrs.activation > 0.0, "activation shouldn't be zero yet");
assert!(attrs.novelty < 1.0, "novelty should decay");
attrs.activation = 1.0;
attrs.novelty = 1.0;
attrs.decay(100_000.0);
assert!(attrs.activation < 0.01);
assert!(attrs.novelty < 0.001);
}
#[test]
fn test_cognitive_attrs_clamp() {
let mut attrs = CognitiveAttrs {
confidence: 1.5,
activation: -0.1,
valence: 2.0,
..Default::default()
};
attrs.clamp();
assert_eq!(attrs.confidence, 1.0);
assert_eq!(attrs.activation, 0.0);
assert_eq!(attrs.valence, 1.0);
}
#[test]
fn test_cognitive_attrs_relevance_score() {
let mut high_activation = CognitiveAttrs::default_for(NodeKind::Goal);
high_activation.activation = 1.0;
high_activation.urgency = 0.8;
let low_activation = CognitiveAttrs::default_for(NodeKind::Goal);
assert!(
high_activation.relevance_score() > low_activation.relevance_score(),
"high activation should have higher relevance"
);
}
#[test]
fn test_edge_kinds_completeness() {
assert_eq!(CognitiveEdgeKind::ALL.len(), CognitiveEdgeKind::COUNT);
for kind in CognitiveEdgeKind::ALL {
let s = kind.as_str();
let parsed = CognitiveEdgeKind::from_str(s).unwrap();
assert_eq!(parsed, kind, "roundtrip failed for {s}");
}
}
#[test]
fn test_edge_activation_transfer() {
assert!(CognitiveEdgeKind::Supports.activation_transfer() > 0.0);
assert!(CognitiveEdgeKind::Contradicts.activation_transfer() < 0.0);
assert!(
CognitiveEdgeKind::Causes.activation_transfer()
>= CognitiveEdgeKind::Supports.activation_transfer()
);
}
#[test]
fn test_edge_inhibitory() {
let inhibitory: Vec<_> = CognitiveEdgeKind::ALL
.iter()
.filter(|k| k.is_inhibitory())
.collect();
assert!(inhibitory.contains(&&CognitiveEdgeKind::Contradicts));
assert!(inhibitory.contains(&&CognitiveEdgeKind::Prevents));
assert!(inhibitory.contains(&&CognitiveEdgeKind::BlocksGoal));
assert!(inhibitory.contains(&&CognitiveEdgeKind::Avoids));
assert!(inhibitory.contains(&&CognitiveEdgeKind::Constrains));
}
#[test]
fn test_cognitive_edge_confirm() {
let src = NodeId::new(NodeKind::Episode, 1);
let dst = NodeId::new(NodeKind::Belief, 1);
let mut edge = CognitiveEdge::new(src, dst, CognitiveEdgeKind::Supports, 0.8);
let initial_confidence = edge.confidence;
edge.confirm();
assert_eq!(edge.observation_count, 2);
assert!(edge.confidence > initial_confidence);
for _ in 0..20 {
edge.confirm();
}
assert!(edge.confidence > 0.95);
}
#[test]
fn test_cognitive_edge_effective_transfer() {
let src = NodeId::new(NodeKind::Episode, 1);
let dst = NodeId::new(NodeKind::Belief, 1);
let edge = CognitiveEdge::new(src, dst, CognitiveEdgeKind::Supports, 0.8);
let transfer = edge.effective_activation_transfer();
assert!((transfer - 0.28).abs() < 1e-10);
}
#[test]
fn test_node_id_allocator() {
let mut alloc = NodeIdAllocator::new();
let id1 = alloc.alloc(NodeKind::Belief);
let id2 = alloc.alloc(NodeKind::Belief);
let id3 = alloc.alloc(NodeKind::Goal);
assert_eq!(id1.kind(), NodeKind::Belief);
assert_eq!(id1.seq(), 1);
assert_eq!(id2.kind(), NodeKind::Belief);
assert_eq!(id2.seq(), 2);
assert_eq!(id3.kind(), NodeKind::Goal);
assert_eq!(id3.seq(), 1);
assert_eq!(alloc.high_water_mark(NodeKind::Belief), 2);
assert_eq!(alloc.high_water_mark(NodeKind::Goal), 1);
assert_eq!(alloc.high_water_mark(NodeKind::Entity), 0);
}
#[test]
fn test_node_id_allocator_restore() {
let marks = vec![
(NodeKind::Belief, 100),
(NodeKind::Goal, 50),
];
let mut alloc = NodeIdAllocator::from_high_water_marks(&marks);
let id = alloc.alloc(NodeKind::Belief);
assert_eq!(id.seq(), 101);
let id = alloc.alloc(NodeKind::Goal);
assert_eq!(id.seq(), 51);
let id = alloc.alloc(NodeKind::Entity);
assert_eq!(id.seq(), 1);
}
#[test]
fn test_cognitive_node_creation() {
let mut alloc = NodeIdAllocator::new();
let id = alloc.alloc(NodeKind::Belief);
let node = CognitiveNode::new(
id,
"User prefers dark mode".to_string(),
NodePayload::Belief(BeliefPayload {
proposition: "User prefers dark mode".to_string(),
log_odds: 0.0,
domain: "preferences".to_string(),
evidence_trail: vec![],
user_confirmed: false,
}),
);
assert_eq!(node.kind(), NodeKind::Belief);
assert!(node.is_persistent());
assert!((node.attrs.confidence - 0.50).abs() < 1e-10); }
#[test]
fn test_routine_next_occurrence() {
let routine = RoutinePayload {
description: "Morning email check".to_string(),
period_secs: 86400.0, phase_offset_secs: 32400.0, reliability: 0.8,
observation_count: 30,
last_triggered: 0.0,
action_description: "check email".to_string(),
weekday_mask: 0x1F, };
let now = 36000.0; let next = routine.next_occurrence(now);
assert!(next > now);
assert!((next - now - 82800.0).abs() < 1.0); }
#[test]
fn test_risk_expected_impact() {
let risk = RiskPayload {
description: "Server might crash".to_string(),
severity: 0.9,
likelihood: 0.3,
mitigation: "Add monitoring".to_string(),
threatened_goals: vec![],
};
assert!((risk.expected_impact() - 0.27).abs() < 1e-10);
}
#[test]
fn test_action_kind_costs() {
assert_eq!(ActionKind::Abstain.base_cost(), 0.0);
assert!(ActionKind::Execute.base_cost() > ActionKind::Communicate.base_cost());
assert!(ActionKind::Communicate.base_cost() > ActionKind::Inform.base_cost());
}
#[test]
fn test_priority_ordering() {
assert!(Priority::Critical > Priority::High);
assert!(Priority::High > Priority::Medium);
assert!(Priority::Medium > Priority::Low);
}
#[test]
fn test_priority_weight() {
assert!((Priority::Critical.weight() - 1.0).abs() < 1e-10);
assert!((Priority::Low.weight() - 0.25).abs() < 1e-10);
}
#[test]
fn test_goal_status_roundtrip() {
let statuses = [
GoalStatus::Active,
GoalStatus::Paused,
GoalStatus::Completed,
GoalStatus::Abandoned,
GoalStatus::Blocked,
];
for s in statuses {
assert_eq!(GoalStatus::from_str(s.as_str()), s);
}
}
#[test]
fn test_task_status_roundtrip() {
let statuses = [
TaskStatus::Pending,
TaskStatus::InProgress,
TaskStatus::Completed,
TaskStatus::Cancelled,
TaskStatus::Blocked,
];
for s in statuses {
assert_eq!(TaskStatus::from_str(s.as_str()), s);
}
}
#[test]
fn test_constraint_types() {
assert_eq!(ConstraintType::from_str("hard"), ConstraintType::Hard);
assert_eq!(ConstraintType::from_str("soft"), ConstraintType::Soft);
assert_eq!(ConstraintType::from_str("unknown"), ConstraintType::Soft);
}
#[test]
fn test_need_categories_roundtrip() {
let categories = [
NeedCategory::Informational,
NeedCategory::Social,
NeedCategory::Emotional,
NeedCategory::Organizational,
NeedCategory::Creative,
NeedCategory::Health,
NeedCategory::Financial,
NeedCategory::Professional,
];
for c in categories {
assert_eq!(NeedCategory::from_str(c.as_str()), c);
}
}
#[test]
fn test_preference_payload_probability() {
let pref = PreferencePayload {
domain: "UI".to_string(),
preferred: "dark mode".to_string(),
dispreferred: Some("light mode".to_string()),
strength: 0.8,
log_odds: 2.0,
observation_count: 10,
};
assert!(pref.probability() > 0.85);
}
#[test]
fn test_node_payload_kind_consistency() {
let payloads: Vec<NodePayload> = vec![
NodePayload::Entity(EntityPayload {
name: "test".into(),
entity_type: "person".into(),
memory_rids: vec![],
}),
NodePayload::Belief(BeliefPayload {
proposition: "test".into(),
log_odds: 0.0,
domain: "test".into(),
evidence_trail: vec![],
user_confirmed: false,
}),
NodePayload::Goal(GoalPayload {
description: "test".into(),
status: GoalStatus::Active,
progress: 0.0,
deadline: None,
priority: Priority::Medium,
parent_goal: None,
completion_criteria: "test".into(),
}),
];
let expected_kinds = [NodeKind::Entity, NodeKind::Belief, NodeKind::Goal];
for (payload, expected) in payloads.iter().zip(expected_kinds.iter()) {
assert_eq!(payload.kind(), *expected);
}
}
#[test]
fn test_serialize_deserialize_payload() {
let original = NodePayload::Belief(BeliefPayload {
proposition: "The sky is blue".to_string(),
log_odds: 3.5,
domain: "science".to_string(),
evidence_trail: vec![
EvidenceEntry {
source: "observation".to_string(),
weight: 2.0,
timestamp: 1000.0,
},
],
user_confirmed: true,
});
let json = serialize_payload(&original);
let restored = deserialize_payload(NodeKind::Belief, &json).unwrap();
if let NodePayload::Belief(b) = restored {
assert_eq!(b.proposition, "The sky is blue");
assert!((b.log_odds - 3.5).abs() < 1e-10);
assert_eq!(b.evidence_trail.len(), 1);
assert!(b.user_confirmed);
} else {
panic!("Expected Belief payload");
}
}
#[test]
fn test_persistent_vs_transient() {
assert!(NodeKind::Entity.is_persistent());
assert!(NodeKind::Belief.is_persistent());
assert!(NodeKind::Goal.is_persistent());
assert!(!NodeKind::IntentHypothesis.is_persistent());
assert!(!NodeKind::ConversationThread.is_persistent());
}
#[test]
fn test_time_sensitive_kinds() {
assert!(NodeKind::Task.is_time_sensitive());
assert!(NodeKind::Opportunity.is_time_sensitive());
assert!(NodeKind::Need.is_time_sensitive());
assert!(NodeKind::Risk.is_time_sensitive());
assert!(!NodeKind::Entity.is_time_sensitive());
assert!(!NodeKind::Belief.is_time_sensitive());
}
#[test]
fn test_action_schema_payload() {
let schema = ActionSchemaPayload {
name: "send_reminder".to_string(),
description: "Send a reminder to the user".to_string(),
action_kind: ActionKind::Communicate,
preconditions: vec![
Precondition {
description: "User has a pending task".to_string(),
node_ref: None,
required: true,
},
],
effects: vec![
Effect {
description: "User is reminded of the task".to_string(),
probability: 0.95,
utility: 0.3,
},
],
confidence_threshold: 0.6,
success_rate: 0.85,
execution_count: 100,
acceptance_count: 85,
};
assert_eq!(schema.action_kind, ActionKind::Communicate);
assert_eq!(schema.preconditions.len(), 1);
assert!(schema.preconditions[0].required);
}
#[test]
fn test_conversation_payload() {
let conv = ConversationPayload {
topic: "Planning vacation".to_string(),
valence_history: vec![0.5, 0.6, 0.3, 0.8],
open_items: vec!["Book hotel".to_string(), "Check flights".to_string()],
turn_count: 12,
started_at: 1000.0,
};
assert_eq!(conv.open_items.len(), 2);
assert_eq!(conv.turn_count, 12);
}
}