1use serde::{Deserialize, Deserializer, Serialize};
8use std::collections::HashMap;
9use std::fmt;
10use uuid::Uuid;
11
12fn deserialize_updated_at<'de, D>(deserializer: D) -> Result<i64, D::Error>
13where
14 D: Deserializer<'de>,
15{
16 use serde::de::{self, Visitor};
17
18 struct TsVisitor;
19 impl<'de> Visitor<'de> for TsVisitor {
20 type Value = i64;
21
22 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
23 f.write_str("unix timestamp or RFC3339 string")
24 }
25
26 fn visit_i64<E: de::Error>(self, v: i64) -> Result<i64, E> {
27 Ok(v)
28 }
29
30 fn visit_u64<E: de::Error>(self, v: u64) -> Result<i64, E> {
31 i64::try_from(v).map_err(de::Error::custom)
32 }
33
34 fn visit_str<E: de::Error>(self, v: &str) -> Result<i64, E> {
35 if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(v) {
36 return Ok(dt.timestamp());
37 }
38 v.parse::<i64>().map_err(de::Error::custom)
39 }
40
41 fn visit_string<E: de::Error>(self, v: String) -> Result<i64, E> {
42 self.visit_str(&v)
43 }
44 }
45
46 deserializer.deserialize_any(TsVisitor)
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum AinlNodeKind {
52 Episode,
53 Semantic,
54 Procedural,
55 Persona,
56 RuntimeState,
58}
59
60impl AinlNodeKind {
61 pub fn as_str(&self) -> &'static str {
62 match self {
63 Self::Episode => "episode",
64 Self::Semantic => "semantic",
65 Self::Procedural => "procedural",
66 Self::Persona => "persona",
67 Self::RuntimeState => "runtime_state",
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(rename_all = "snake_case")]
75pub enum MemoryCategory {
76 Persona,
77 Semantic,
78 Episodic,
79 Procedural,
80 RuntimeState,
82}
83
84impl MemoryCategory {
85 pub fn from_node_type(node_type: &AinlNodeType) -> Self {
86 match node_type {
87 AinlNodeType::Episode { .. } => MemoryCategory::Episodic,
88 AinlNodeType::Semantic { .. } => MemoryCategory::Semantic,
89 AinlNodeType::Procedural { .. } => MemoryCategory::Procedural,
90 AinlNodeType::Persona { .. } => MemoryCategory::Persona,
91 AinlNodeType::RuntimeState { .. } => MemoryCategory::RuntimeState,
92 }
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
97#[serde(rename_all = "snake_case")]
98pub enum PersonaLayer {
99 #[default]
100 Base,
101 Delta,
102 Injection,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
106#[serde(rename_all = "snake_case")]
107pub enum PersonaSource {
108 SystemDefault,
109 #[default]
110 UserConfigured,
111 Evolved,
112 Feedback,
113 Injection,
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(rename_all = "snake_case")]
118pub enum Sentiment {
119 Positive,
120 Neutral,
121 Negative,
122 Mixed,
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
126#[serde(rename_all = "snake_case")]
127pub enum ProcedureType {
128 #[default]
129 ToolSequence,
130 ResponsePattern,
131 WorkflowStep,
132 BehavioralRule,
133}
134
135#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
137pub struct StrengthEvent {
138 pub delta: f32,
139 pub reason: String,
140 pub episode_id: String,
141 pub timestamp: u64,
142}
143
144fn default_importance_score() -> f32 {
145 0.5
146}
147
148fn default_semantic_confidence() -> f32 {
149 0.7
150}
151
152fn default_decay_eligible() -> bool {
153 true
154}
155
156fn default_success_rate() -> f32 {
157 0.5
158}
159
160fn default_strength_floor() -> f32 {
161 0.0
162}
163
164#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
166pub struct PersonaNode {
167 pub trait_name: String,
168 pub strength: f32,
169 #[serde(default)]
170 pub learned_from: Vec<Uuid>,
171 #[serde(default)]
172 pub layer: PersonaLayer,
173 #[serde(default)]
174 pub source: PersonaSource,
175 #[serde(default = "default_strength_floor")]
176 pub strength_floor: f32,
177 #[serde(default)]
178 pub locked: bool,
179 #[serde(default)]
180 pub relevance_score: f32,
181 #[serde(default)]
182 pub provenance_episode_ids: Vec<String>,
183 #[serde(default)]
184 pub evolution_log: Vec<StrengthEvent>,
185 #[serde(default)]
187 pub axis_scores: HashMap<String, f32>,
188 #[serde(default)]
189 pub evolution_cycle: u32,
190 #[serde(default)]
192 pub last_evolved: String,
193 #[serde(default)]
195 pub agent_id: String,
196 #[serde(default)]
198 pub dominant_axes: Vec<String>,
199}
200
201#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
203pub struct SemanticNode {
204 pub fact: String,
205 #[serde(default = "default_semantic_confidence")]
206 pub confidence: f32,
207 pub source_turn_id: Uuid,
208 #[serde(default)]
209 pub topic_cluster: Option<String>,
210 #[serde(default)]
211 pub source_episode_id: String,
212 #[serde(default)]
213 pub contradiction_ids: Vec<String>,
214 #[serde(default)]
215 pub last_referenced_at: u64,
216 #[serde(default)]
219 pub reference_count: u32,
220 #[serde(default = "default_decay_eligible")]
221 pub decay_eligible: bool,
222 #[serde(default)]
224 pub tags: Vec<String>,
225 #[serde(default)]
232 pub recurrence_count: u32,
233 #[serde(rename = "_last_ref_snapshot", default)]
235 pub last_ref_snapshot: u32,
236}
237
238#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
240pub struct EpisodicNode {
241 pub turn_id: Uuid,
242 pub timestamp: i64,
243 #[serde(default)]
244 pub tool_calls: Vec<String>,
245 #[serde(default)]
246 pub delegation_to: Option<String>,
247 #[serde(skip_serializing_if = "Option::is_none")]
248 pub trace_event: Option<serde_json::Value>,
249 #[serde(default)]
250 pub turn_index: u32,
251 #[serde(default)]
252 pub user_message_tokens: u32,
253 #[serde(default)]
254 pub assistant_response_tokens: u32,
255 #[serde(default)]
257 pub tools_invoked: Vec<String>,
258 #[serde(default)]
261 pub persona_signals_emitted: Vec<String>,
262 #[serde(default)]
263 pub sentiment: Option<Sentiment>,
264 #[serde(default)]
265 pub flagged: bool,
266 #[serde(default)]
267 pub conversation_id: String,
268 #[serde(default)]
269 pub follows_episode_id: Option<String>,
270 #[serde(default, skip_serializing_if = "Option::is_none")]
272 pub user_message: Option<String>,
273 #[serde(default, skip_serializing_if = "Option::is_none")]
275 pub assistant_response: Option<String>,
276}
277
278impl EpisodicNode {
279 pub fn effective_tools(&self) -> &[String] {
281 if !self.tools_invoked.is_empty() {
282 &self.tools_invoked
283 } else {
284 &self.tool_calls
285 }
286 }
287}
288
289#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
291pub struct ProceduralNode {
292 pub pattern_name: String,
293 #[serde(default)]
294 pub compiled_graph: Vec<u8>,
295 #[serde(default, skip_serializing_if = "Vec::is_empty")]
296 pub tool_sequence: Vec<String>,
297 #[serde(default, skip_serializing_if = "Option::is_none")]
298 pub confidence: Option<f32>,
299 #[serde(default)]
300 pub procedure_type: ProcedureType,
301 #[serde(default)]
302 pub trigger_conditions: Vec<String>,
303 #[serde(default)]
304 pub success_count: u32,
305 #[serde(default)]
306 pub failure_count: u32,
307 #[serde(default = "default_success_rate")]
308 pub success_rate: f32,
309 #[serde(default)]
310 pub last_invoked_at: u64,
311 #[serde(default)]
312 pub reinforcement_episode_ids: Vec<String>,
313 #[serde(default)]
314 pub suppression_episode_ids: Vec<String>,
315 #[serde(default)]
317 pub patch_version: u32,
318 #[serde(default)]
320 pub fitness: Option<f32>,
321 #[serde(default)]
323 pub declared_reads: Vec<String>,
324 #[serde(default)]
326 pub retired: bool,
327 #[serde(default)]
329 pub label: String,
330 #[serde(default, skip_serializing_if = "Option::is_none")]
332 pub trace_id: Option<String>,
333}
334
335impl ProceduralNode {
336 pub fn recompute_success_rate(&mut self) {
337 let total = self.success_count.saturating_add(self.failure_count);
338 self.success_rate = if total == 0 {
339 0.5
340 } else {
341 self.success_count as f32 / total as f32
342 };
343 }
344}
345
346#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
349pub struct RuntimeStateNode {
350 pub agent_id: String,
351 #[serde(default)]
352 pub turn_count: u64,
353 #[serde(default, alias = "last_extraction_turn")]
354 pub last_extraction_at_turn: u64,
355 #[serde(default, alias = "last_persona_prompt")]
357 pub persona_snapshot_json: Option<String>,
358 #[serde(default, deserialize_with = "deserialize_updated_at")]
359 pub updated_at: i64,
360}
361
362#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
364#[serde(tag = "type", rename_all = "snake_case")]
365pub enum AinlNodeType {
366 Episode {
368 #[serde(flatten)]
369 episodic: EpisodicNode,
370 },
371
372 Semantic {
374 #[serde(flatten)]
375 semantic: SemanticNode,
376 },
377
378 Procedural {
380 #[serde(flatten)]
381 procedural: ProceduralNode,
382 },
383
384 Persona {
386 #[serde(flatten)]
387 persona: PersonaNode,
388 },
389
390 RuntimeState { runtime_state: RuntimeStateNode },
392}
393
394#[derive(Serialize, Debug, Clone, PartialEq)]
396pub struct AinlMemoryNode {
397 pub id: Uuid,
398 pub memory_category: MemoryCategory,
399 pub importance_score: f32,
400 pub agent_id: String,
401 pub node_type: AinlNodeType,
402 pub edges: Vec<AinlEdge>,
403}
404
405#[derive(Deserialize)]
406struct AinlMemoryNodeWire {
407 id: Uuid,
408 #[serde(default)]
409 memory_category: Option<MemoryCategory>,
410 #[serde(default)]
411 importance_score: Option<f32>,
412 #[serde(default)]
413 agent_id: Option<String>,
414 node_type: AinlNodeType,
415 #[serde(default)]
416 edges: Vec<AinlEdge>,
417}
418
419impl From<AinlMemoryNodeWire> for AinlMemoryNode {
420 fn from(w: AinlMemoryNodeWire) -> Self {
421 let memory_category = w
422 .memory_category
423 .unwrap_or_else(|| MemoryCategory::from_node_type(&w.node_type));
424 let importance_score = w.importance_score.unwrap_or_else(default_importance_score);
425 Self {
426 id: w.id,
427 memory_category,
428 importance_score,
429 agent_id: w.agent_id.unwrap_or_default(),
430 node_type: w.node_type,
431 edges: w.edges,
432 }
433 }
434}
435
436impl<'de> Deserialize<'de> for AinlMemoryNode {
437 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
438 where
439 D: serde::Deserializer<'de>,
440 {
441 let w = AinlMemoryNodeWire::deserialize(deserializer)?;
442 Ok(Self::from(w))
443 }
444}
445
446#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
448pub struct AinlEdge {
449 pub target_id: Uuid,
451
452 pub label: String,
454}
455
456impl AinlMemoryNode {
457 fn base(
458 memory_category: MemoryCategory,
459 importance_score: f32,
460 agent_id: String,
461 node_type: AinlNodeType,
462 ) -> Self {
463 Self {
464 id: Uuid::new_v4(),
465 memory_category,
466 importance_score,
467 agent_id,
468 node_type,
469 edges: Vec::new(),
470 }
471 }
472
473 pub fn new_episode(
475 turn_id: Uuid,
476 timestamp: i64,
477 tool_calls: Vec<String>,
478 delegation_to: Option<String>,
479 trace_event: Option<serde_json::Value>,
480 ) -> Self {
481 let tools_invoked = tool_calls.clone();
482 let episodic = EpisodicNode {
483 turn_id,
484 timestamp,
485 tool_calls,
486 delegation_to,
487 trace_event,
488 turn_index: 0,
489 user_message_tokens: 0,
490 assistant_response_tokens: 0,
491 tools_invoked,
492 persona_signals_emitted: Vec::new(),
493 sentiment: None,
494 flagged: false,
495 conversation_id: String::new(),
496 follows_episode_id: None,
497 user_message: None,
498 assistant_response: None,
499 };
500 Self::base(
501 MemoryCategory::Episodic,
502 default_importance_score(),
503 String::new(),
504 AinlNodeType::Episode { episodic },
505 )
506 }
507
508 pub fn new_fact(fact: String, confidence: f32, source_turn_id: Uuid) -> Self {
510 let semantic = SemanticNode {
511 fact,
512 confidence,
513 source_turn_id,
514 topic_cluster: None,
515 source_episode_id: String::new(),
516 contradiction_ids: Vec::new(),
517 last_referenced_at: 0,
518 reference_count: 0,
519 decay_eligible: true,
520 tags: Vec::new(),
521 recurrence_count: 0,
522 last_ref_snapshot: 0,
523 };
524 Self::base(
525 MemoryCategory::Semantic,
526 default_importance_score(),
527 String::new(),
528 AinlNodeType::Semantic { semantic },
529 )
530 }
531
532 pub fn new_pattern(pattern_name: String, compiled_graph: Vec<u8>) -> Self {
534 let mut procedural = ProceduralNode {
535 pattern_name,
536 compiled_graph,
537 tool_sequence: Vec::new(),
538 confidence: None,
539 procedure_type: ProcedureType::default(),
540 trigger_conditions: Vec::new(),
541 success_count: 0,
542 failure_count: 0,
543 success_rate: default_success_rate(),
544 last_invoked_at: 0,
545 reinforcement_episode_ids: Vec::new(),
546 suppression_episode_ids: Vec::new(),
547 patch_version: 1,
548 fitness: None,
549 declared_reads: Vec::new(),
550 retired: false,
551 label: String::new(),
552 trace_id: None,
553 };
554 procedural.recompute_success_rate();
555 Self::base(
556 MemoryCategory::Procedural,
557 default_importance_score(),
558 String::new(),
559 AinlNodeType::Procedural { procedural },
560 )
561 }
562
563 pub fn new_procedural_tools(
565 pattern_name: String,
566 tool_sequence: Vec<String>,
567 confidence: f32,
568 ) -> Self {
569 let mut procedural = ProceduralNode {
570 pattern_name,
571 compiled_graph: Vec::new(),
572 tool_sequence,
573 confidence: Some(confidence),
574 procedure_type: ProcedureType::ToolSequence,
575 trigger_conditions: Vec::new(),
576 success_count: 0,
577 failure_count: 0,
578 success_rate: default_success_rate(),
579 last_invoked_at: 0,
580 reinforcement_episode_ids: Vec::new(),
581 suppression_episode_ids: Vec::new(),
582 patch_version: 1,
583 fitness: None,
584 declared_reads: Vec::new(),
585 retired: false,
586 label: String::new(),
587 trace_id: None,
588 };
589 procedural.recompute_success_rate();
590 Self::base(
591 MemoryCategory::Procedural,
592 default_importance_score(),
593 String::new(),
594 AinlNodeType::Procedural { procedural },
595 )
596 }
597
598 pub fn new_persona(trait_name: String, strength: f32, learned_from: Vec<Uuid>) -> Self {
600 let persona = PersonaNode {
601 trait_name,
602 strength,
603 learned_from,
604 layer: PersonaLayer::default(),
605 source: PersonaSource::default(),
606 strength_floor: default_strength_floor(),
607 locked: false,
608 relevance_score: 0.0,
609 provenance_episode_ids: Vec::new(),
610 evolution_log: Vec::new(),
611 axis_scores: HashMap::new(),
612 evolution_cycle: 0,
613 last_evolved: String::new(),
614 agent_id: String::new(),
615 dominant_axes: Vec::new(),
616 };
617 Self::base(
618 MemoryCategory::Persona,
619 default_importance_score(),
620 String::new(),
621 AinlNodeType::Persona { persona },
622 )
623 }
624
625 pub fn episodic(&self) -> Option<&EpisodicNode> {
626 match &self.node_type {
627 AinlNodeType::Episode { episodic } => Some(episodic),
628 _ => None,
629 }
630 }
631
632 pub fn semantic(&self) -> Option<&SemanticNode> {
633 match &self.node_type {
634 AinlNodeType::Semantic { semantic } => Some(semantic),
635 _ => None,
636 }
637 }
638
639 pub fn procedural(&self) -> Option<&ProceduralNode> {
640 match &self.node_type {
641 AinlNodeType::Procedural { procedural } => Some(procedural),
642 _ => None,
643 }
644 }
645
646 pub fn persona(&self) -> Option<&PersonaNode> {
647 match &self.node_type {
648 AinlNodeType::Persona { persona } => Some(persona),
649 _ => None,
650 }
651 }
652
653 pub fn add_edge(&mut self, target_id: Uuid, label: impl Into<String>) {
655 self.edges.push(AinlEdge {
656 target_id,
657 label: label.into(),
658 });
659 }
660}