1use ainl_contracts::{TrajectoryOutcome, TrajectoryStep};
9use serde::{Deserialize, Deserializer, Serialize};
10use std::collections::HashMap;
11use std::fmt;
12use uuid::Uuid;
13
14fn deserialize_updated_at<'de, D>(deserializer: D) -> Result<i64, D::Error>
15where
16 D: Deserializer<'de>,
17{
18 use serde::de::{self, Visitor};
19
20 struct TsVisitor;
21 impl<'de> Visitor<'de> for TsVisitor {
22 type Value = i64;
23
24 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
25 f.write_str("unix timestamp or RFC3339 string")
26 }
27
28 fn visit_i64<E: de::Error>(self, v: i64) -> Result<i64, E> {
29 Ok(v)
30 }
31
32 fn visit_u64<E: de::Error>(self, v: u64) -> Result<i64, E> {
33 i64::try_from(v).map_err(de::Error::custom)
34 }
35
36 fn visit_str<E: de::Error>(self, v: &str) -> Result<i64, E> {
37 if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(v) {
38 return Ok(dt.timestamp());
39 }
40 v.parse::<i64>().map_err(de::Error::custom)
41 }
42
43 fn visit_string<E: de::Error>(self, v: String) -> Result<i64, E> {
44 self.visit_str(&v)
45 }
46 }
47
48 deserializer.deserialize_any(TsVisitor)
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum AinlNodeKind {
54 Episode,
55 Semantic,
56 Procedural,
57 Persona,
58 RuntimeState,
60 Trajectory,
62 Failure,
64}
65
66impl AinlNodeKind {
67 pub fn as_str(&self) -> &'static str {
68 match self {
69 Self::Episode => "episode",
70 Self::Semantic => "semantic",
71 Self::Procedural => "procedural",
72 Self::Persona => "persona",
73 Self::RuntimeState => "runtime_state",
74 Self::Trajectory => "trajectory",
75 Self::Failure => "failure",
76 }
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "snake_case")]
83pub enum MemoryCategory {
84 Persona,
85 Semantic,
86 Episodic,
87 Procedural,
88 RuntimeState,
90 Trajectory,
92 Failure,
94}
95
96impl MemoryCategory {
97 pub fn from_node_type(node_type: &AinlNodeType) -> Self {
98 match node_type {
99 AinlNodeType::Episode { .. } => MemoryCategory::Episodic,
100 AinlNodeType::Semantic { .. } => MemoryCategory::Semantic,
101 AinlNodeType::Procedural { .. } => MemoryCategory::Procedural,
102 AinlNodeType::Persona { .. } => MemoryCategory::Persona,
103 AinlNodeType::RuntimeState { .. } => MemoryCategory::RuntimeState,
104 AinlNodeType::Trajectory { .. } => MemoryCategory::Trajectory,
105 AinlNodeType::Failure { .. } => MemoryCategory::Failure,
106 }
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
111#[serde(rename_all = "snake_case")]
112pub enum PersonaLayer {
113 #[default]
114 Base,
115 Delta,
116 Injection,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
120#[serde(rename_all = "snake_case")]
121pub enum PersonaSource {
122 SystemDefault,
123 #[default]
124 UserConfigured,
125 Evolved,
126 Feedback,
127 Injection,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
131#[serde(rename_all = "snake_case")]
132pub enum Sentiment {
133 Positive,
134 Neutral,
135 Negative,
136 Mixed,
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
140#[serde(rename_all = "snake_case")]
141pub enum ProcedureType {
142 #[default]
143 ToolSequence,
144 ResponsePattern,
145 WorkflowStep,
146 BehavioralRule,
147}
148
149#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
151pub struct StrengthEvent {
152 pub delta: f32,
153 pub reason: String,
154 pub episode_id: String,
155 pub timestamp: u64,
156}
157
158fn default_importance_score() -> f32 {
159 0.5
160}
161
162fn default_semantic_confidence() -> f32 {
163 0.7
164}
165
166fn default_decay_eligible() -> bool {
167 true
168}
169
170fn default_success_rate() -> f32 {
171 0.5
172}
173
174fn default_procedural_prompt_eligible() -> bool {
175 true
176}
177
178fn default_strength_floor() -> f32 {
179 0.0
180}
181
182#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
184pub struct PersonaNode {
185 pub trait_name: String,
186 pub strength: f32,
187 #[serde(default)]
188 pub learned_from: Vec<Uuid>,
189 #[serde(default)]
190 pub layer: PersonaLayer,
191 #[serde(default)]
192 pub source: PersonaSource,
193 #[serde(default = "default_strength_floor")]
194 pub strength_floor: f32,
195 #[serde(default)]
196 pub locked: bool,
197 #[serde(default)]
198 pub relevance_score: f32,
199 #[serde(default)]
200 pub provenance_episode_ids: Vec<String>,
201 #[serde(default)]
202 pub evolution_log: Vec<StrengthEvent>,
203 #[serde(default)]
205 pub axis_scores: HashMap<String, f32>,
206 #[serde(default)]
207 pub evolution_cycle: u32,
208 #[serde(default)]
210 pub last_evolved: String,
211 #[serde(default)]
213 pub agent_id: String,
214 #[serde(default)]
216 pub dominant_axes: Vec<String>,
217}
218
219#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
221pub struct SemanticNode {
222 pub fact: String,
223 #[serde(default = "default_semantic_confidence")]
224 pub confidence: f32,
225 pub source_turn_id: Uuid,
226 #[serde(default)]
227 pub topic_cluster: Option<String>,
228 #[serde(default)]
229 pub source_episode_id: String,
230 #[serde(default)]
231 pub contradiction_ids: Vec<String>,
232 #[serde(default)]
233 pub last_referenced_at: u64,
234 #[serde(default)]
237 pub reference_count: u32,
238 #[serde(default = "default_decay_eligible")]
239 pub decay_eligible: bool,
240 #[serde(default)]
242 pub tags: Vec<String>,
243 #[serde(default)]
250 pub recurrence_count: u32,
251 #[serde(rename = "_last_ref_snapshot", default)]
253 pub last_ref_snapshot: u32,
254}
255
256#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
258pub struct EpisodicNode {
259 pub turn_id: Uuid,
260 pub timestamp: i64,
261 #[serde(default)]
262 pub tool_calls: Vec<String>,
263 #[serde(default)]
264 pub delegation_to: Option<String>,
265 #[serde(skip_serializing_if = "Option::is_none")]
266 pub trace_event: Option<serde_json::Value>,
267 #[serde(default)]
268 pub turn_index: u32,
269 #[serde(default)]
270 pub user_message_tokens: u32,
271 #[serde(default)]
272 pub assistant_response_tokens: u32,
273 #[serde(default)]
275 pub tools_invoked: Vec<String>,
276 #[serde(default)]
279 pub persona_signals_emitted: Vec<String>,
280 #[serde(default)]
281 pub sentiment: Option<Sentiment>,
282 #[serde(default)]
283 pub flagged: bool,
284 #[serde(default)]
285 pub conversation_id: String,
286 #[serde(default)]
287 pub follows_episode_id: Option<String>,
288 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub user_message: Option<String>,
291 #[serde(default, skip_serializing_if = "Option::is_none")]
293 pub assistant_response: Option<String>,
294 #[serde(default, skip_serializing_if = "Vec::is_empty")]
296 pub tags: Vec<String>,
297 #[serde(default, skip_serializing_if = "Option::is_none")]
300 pub vitals_gate: Option<String>,
301 #[serde(default, skip_serializing_if = "Option::is_none")]
303 pub vitals_phase: Option<String>,
304 #[serde(default, skip_serializing_if = "Option::is_none")]
306 pub vitals_trust: Option<f32>,
307}
308
309impl EpisodicNode {
310 pub fn effective_tools(&self) -> &[String] {
312 if !self.tools_invoked.is_empty() {
313 &self.tools_invoked
314 } else {
315 &self.tool_calls
316 }
317 }
318}
319
320#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
322pub struct ProceduralNode {
323 pub pattern_name: String,
324 #[serde(default)]
325 pub compiled_graph: Vec<u8>,
326 #[serde(default, skip_serializing_if = "Vec::is_empty")]
327 pub tool_sequence: Vec<String>,
328 #[serde(default, skip_serializing_if = "Option::is_none")]
329 pub confidence: Option<f32>,
330 #[serde(default)]
331 pub procedure_type: ProcedureType,
332 #[serde(default)]
333 pub trigger_conditions: Vec<String>,
334 #[serde(default)]
335 pub success_count: u32,
336 #[serde(default)]
337 pub failure_count: u32,
338 #[serde(default = "default_success_rate")]
339 pub success_rate: f32,
340 #[serde(default)]
341 pub last_invoked_at: u64,
342 #[serde(default)]
343 pub reinforcement_episode_ids: Vec<String>,
344 #[serde(default)]
345 pub suppression_episode_ids: Vec<String>,
346 #[serde(default)]
348 pub patch_version: u32,
349 #[serde(default)]
351 pub fitness: Option<f32>,
352 #[serde(default)]
354 pub declared_reads: Vec<String>,
355 #[serde(default)]
357 pub retired: bool,
358 #[serde(default)]
360 pub label: String,
361 #[serde(default, skip_serializing_if = "Option::is_none")]
363 pub trace_id: Option<String>,
364 #[serde(default)]
366 pub pattern_observation_count: u32,
367 #[serde(default = "default_procedural_prompt_eligible")]
371 pub prompt_eligible: bool,
372}
373
374impl ProceduralNode {
375 pub fn recompute_success_rate(&mut self) {
376 let total = self.success_count.saturating_add(self.failure_count);
377 self.success_rate = if total == 0 {
378 0.5
379 } else {
380 self.success_count as f32 / total as f32
381 };
382 }
383}
384
385#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
388pub struct RuntimeStateNode {
389 pub agent_id: String,
390 #[serde(default)]
391 pub turn_count: u64,
392 #[serde(default, alias = "last_extraction_turn")]
393 pub last_extraction_at_turn: u64,
394 #[serde(default, alias = "last_persona_prompt")]
396 pub persona_snapshot_json: Option<String>,
397 #[serde(default, deserialize_with = "deserialize_updated_at")]
398 pub updated_at: i64,
399}
400
401#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
403pub struct TrajectoryNode {
404 pub episode_id: Uuid,
406 pub recorded_at: i64,
408 #[serde(default)]
409 pub session_id: String,
410 #[serde(default)]
411 pub project_id: Option<String>,
412 #[serde(default)]
413 pub ainl_source_hash: Option<String>,
414 pub outcome: TrajectoryOutcome,
415 #[serde(default)]
416 pub steps: Vec<TrajectoryStep>,
417 #[serde(default)]
418 pub duration_ms: u64,
419 #[serde(default, skip_serializing_if = "Option::is_none")]
421 pub frame_vars: Option<serde_json::Value>,
422 #[serde(default, skip_serializing_if = "Option::is_none")]
424 pub fitness_delta: Option<f32>,
425}
426
427#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
429pub struct FailureNode {
430 pub recorded_at: i64,
432 pub source: String,
434 #[serde(default)]
435 pub tool_name: Option<String>,
436 #[serde(default, skip_serializing_if = "Option::is_none")]
438 pub source_namespace: Option<String>,
439 #[serde(default, skip_serializing_if = "Option::is_none")]
441 pub source_tool: Option<String>,
442 pub message: String,
443 #[serde(default)]
444 pub session_id: Option<String>,
445}
446
447#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
449#[serde(tag = "type", rename_all = "snake_case")]
450pub enum AinlNodeType {
451 Episode {
453 #[serde(flatten)]
454 episodic: EpisodicNode,
455 },
456
457 Semantic {
459 #[serde(flatten)]
460 semantic: SemanticNode,
461 },
462
463 Procedural {
465 #[serde(flatten)]
466 procedural: ProceduralNode,
467 },
468
469 Persona {
471 #[serde(flatten)]
472 persona: PersonaNode,
473 },
474
475 RuntimeState { runtime_state: RuntimeStateNode },
477
478 Trajectory { trajectory: TrajectoryNode },
480
481 Failure { failure: FailureNode },
483}
484
485#[derive(Serialize, Debug, Clone, PartialEq)]
487pub struct AinlMemoryNode {
488 pub id: Uuid,
489 pub memory_category: MemoryCategory,
490 pub importance_score: f32,
491 pub agent_id: String,
492 #[serde(default, skip_serializing_if = "Option::is_none")]
494 pub project_id: Option<String>,
495 pub node_type: AinlNodeType,
496 pub edges: Vec<AinlEdge>,
497 #[serde(default, skip_serializing_if = "Option::is_none")]
500 pub plugin_data: Option<serde_json::Value>,
501}
502
503#[derive(Deserialize)]
504struct AinlMemoryNodeWire {
505 id: Uuid,
506 #[serde(default)]
507 memory_category: Option<MemoryCategory>,
508 #[serde(default)]
509 importance_score: Option<f32>,
510 #[serde(default)]
511 agent_id: Option<String>,
512 #[serde(default)]
513 project_id: Option<String>,
514 node_type: AinlNodeType,
515 #[serde(default)]
516 edges: Vec<AinlEdge>,
517 #[serde(default)]
518 plugin_data: Option<serde_json::Value>,
519}
520
521impl From<AinlMemoryNodeWire> for AinlMemoryNode {
522 fn from(w: AinlMemoryNodeWire) -> Self {
523 let memory_category = w
524 .memory_category
525 .unwrap_or_else(|| MemoryCategory::from_node_type(&w.node_type));
526 let importance_score = w.importance_score.unwrap_or_else(default_importance_score);
527 Self {
528 id: w.id,
529 memory_category,
530 importance_score,
531 agent_id: w.agent_id.unwrap_or_default(),
532 project_id: w
533 .project_id
534 .as_ref()
535 .map(|s| s.trim().to_string())
536 .filter(|s| !s.is_empty()),
537 node_type: w.node_type,
538 edges: w.edges,
539 plugin_data: w.plugin_data,
540 }
541 }
542}
543
544impl<'de> Deserialize<'de> for AinlMemoryNode {
545 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
546 where
547 D: serde::Deserializer<'de>,
548 {
549 let w = AinlMemoryNodeWire::deserialize(deserializer)?;
550 Ok(Self::from(w))
551 }
552}
553
554#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
556pub struct AinlEdge {
557 pub target_id: Uuid,
559
560 pub label: String,
562}
563
564impl AinlMemoryNode {
565 fn base(
566 memory_category: MemoryCategory,
567 importance_score: f32,
568 agent_id: String,
569 node_type: AinlNodeType,
570 ) -> Self {
571 Self {
572 id: Uuid::new_v4(),
573 memory_category,
574 importance_score,
575 agent_id,
576 project_id: None,
577 node_type,
578 edges: Vec::new(),
579 plugin_data: None,
580 }
581 }
582
583 pub fn new_episode(
585 turn_id: Uuid,
586 timestamp: i64,
587 tool_calls: Vec<String>,
588 delegation_to: Option<String>,
589 trace_event: Option<serde_json::Value>,
590 ) -> Self {
591 let tools_invoked = tool_calls.clone();
592 let episodic = EpisodicNode {
593 turn_id,
594 timestamp,
595 tool_calls,
596 delegation_to,
597 trace_event,
598 turn_index: 0,
599 user_message_tokens: 0,
600 assistant_response_tokens: 0,
601 tools_invoked,
602 persona_signals_emitted: Vec::new(),
603 sentiment: None,
604 flagged: false,
605 conversation_id: String::new(),
606 follows_episode_id: None,
607 user_message: None,
608 assistant_response: None,
609 tags: Vec::new(),
610 vitals_gate: None,
611 vitals_phase: None,
612 vitals_trust: None,
613 };
614 Self::base(
615 MemoryCategory::Episodic,
616 default_importance_score(),
617 String::new(),
618 AinlNodeType::Episode { episodic },
619 )
620 }
621
622 pub fn new_fact(fact: String, confidence: f32, source_turn_id: Uuid) -> Self {
624 let semantic = SemanticNode {
625 fact,
626 confidence,
627 source_turn_id,
628 topic_cluster: None,
629 source_episode_id: String::new(),
630 contradiction_ids: Vec::new(),
631 last_referenced_at: 0,
632 reference_count: 0,
633 decay_eligible: true,
634 tags: Vec::new(),
635 recurrence_count: 0,
636 last_ref_snapshot: 0,
637 };
638 Self::base(
639 MemoryCategory::Semantic,
640 default_importance_score(),
641 String::new(),
642 AinlNodeType::Semantic { semantic },
643 )
644 }
645
646 pub fn new_pattern(pattern_name: String, compiled_graph: Vec<u8>) -> Self {
648 let mut procedural = ProceduralNode {
649 pattern_name,
650 compiled_graph,
651 tool_sequence: Vec::new(),
652 confidence: None,
653 procedure_type: ProcedureType::default(),
654 trigger_conditions: Vec::new(),
655 success_count: 0,
656 failure_count: 0,
657 success_rate: default_success_rate(),
658 last_invoked_at: 0,
659 reinforcement_episode_ids: Vec::new(),
660 suppression_episode_ids: Vec::new(),
661 patch_version: 1,
662 fitness: None,
663 declared_reads: Vec::new(),
664 retired: false,
665 label: String::new(),
666 trace_id: None,
667 pattern_observation_count: 0,
668 prompt_eligible: true,
669 };
670 procedural.recompute_success_rate();
671 Self::base(
672 MemoryCategory::Procedural,
673 default_importance_score(),
674 String::new(),
675 AinlNodeType::Procedural { procedural },
676 )
677 }
678
679 pub fn new_procedural_tools(
681 pattern_name: String,
682 tool_sequence: Vec<String>,
683 confidence: f32,
684 ) -> Self {
685 use crate::pattern_promotion;
686 let c = confidence.clamp(0.0, 1.0);
687 let ema0 = pattern_promotion::ema_fitness_update(None, c);
688 let mut procedural = ProceduralNode {
689 pattern_name,
690 compiled_graph: Vec::new(),
691 tool_sequence,
692 confidence: Some(c),
693 procedure_type: ProcedureType::ToolSequence,
694 trigger_conditions: Vec::new(),
695 success_count: 0,
696 failure_count: 0,
697 success_rate: default_success_rate(),
698 last_invoked_at: 0,
699 reinforcement_episode_ids: Vec::new(),
700 suppression_episode_ids: Vec::new(),
701 patch_version: 1,
702 fitness: Some(ema0),
703 declared_reads: Vec::new(),
704 retired: false,
705 label: String::new(),
706 trace_id: None,
707 pattern_observation_count: 1,
708 prompt_eligible: false,
709 };
710 procedural.recompute_success_rate();
711 Self::base(
712 MemoryCategory::Procedural,
713 default_importance_score(),
714 String::new(),
715 AinlNodeType::Procedural { procedural },
716 )
717 }
718
719 pub fn new_persona(trait_name: String, strength: f32, learned_from: Vec<Uuid>) -> Self {
721 let persona = PersonaNode {
722 trait_name,
723 strength,
724 learned_from,
725 layer: PersonaLayer::default(),
726 source: PersonaSource::default(),
727 strength_floor: default_strength_floor(),
728 locked: false,
729 relevance_score: 0.0,
730 provenance_episode_ids: Vec::new(),
731 evolution_log: Vec::new(),
732 axis_scores: HashMap::new(),
733 evolution_cycle: 0,
734 last_evolved: String::new(),
735 agent_id: String::new(),
736 dominant_axes: Vec::new(),
737 };
738 Self::base(
739 MemoryCategory::Persona,
740 default_importance_score(),
741 String::new(),
742 AinlNodeType::Persona { persona },
743 )
744 }
745
746 pub fn new_trajectory(trajectory: TrajectoryNode, agent_id: impl Into<String>) -> Self {
748 Self::base(
749 MemoryCategory::Trajectory,
750 default_importance_score(),
751 agent_id.into(),
752 AinlNodeType::Trajectory { trajectory },
753 )
754 }
755
756 pub fn new_loop_guard_failure(
758 verdict_label: &str,
759 tool_name: Option<&str>,
760 message: impl Into<String>,
761 session_id: Option<&str>,
762 ) -> Self {
763 let recorded_at = chrono::Utc::now().timestamp();
764 let source = format!("loop_guard:{verdict_label}");
765 let failure = FailureNode {
766 recorded_at,
767 source,
768 tool_name: tool_name.map(str::to_string),
769 source_namespace: None,
770 source_tool: None,
771 message: message.into(),
772 session_id: session_id.map(str::to_string),
773 };
774 Self::base(
775 MemoryCategory::Failure,
776 default_importance_score(),
777 String::new(),
778 AinlNodeType::Failure { failure },
779 )
780 }
781
782 pub fn new_tool_execution_failure(
786 tool_name: &str,
787 message: impl Into<String>,
788 session_id: Option<&str>,
789 ) -> Self {
790 Self::new_tool_execution_failure_with_source(
791 tool_name,
792 message,
793 session_id,
794 None::<&str>,
795 None::<&str>,
796 )
797 }
798
799 pub fn new_tool_execution_failure_with_source(
801 tool_name: &str,
802 message: impl Into<String>,
803 session_id: Option<&str>,
804 source_namespace: Option<&str>,
805 source_tool: Option<&str>,
806 ) -> Self {
807 let recorded_at = chrono::Utc::now().timestamp();
808 let source = "tool_runner:error".to_string();
809 let failure = FailureNode {
810 recorded_at,
811 source,
812 tool_name: Some(tool_name.to_string()),
813 source_namespace: source_namespace.map(str::to_string),
814 source_tool: source_tool.map(str::to_string),
815 message: message.into(),
816 session_id: session_id.map(str::to_string),
817 };
818 Self::base(
819 MemoryCategory::Failure,
820 default_importance_score(),
821 String::new(),
822 AinlNodeType::Failure { failure },
823 )
824 }
825
826 pub fn new_agent_loop_precheck_failure(
830 kind: &str,
831 tool_name: &str,
832 message: impl Into<String>,
833 session_id: Option<&str>,
834 ) -> Self {
835 let recorded_at = chrono::Utc::now().timestamp();
836 let source = format!("agent_loop:{kind}");
837 let failure = FailureNode {
838 recorded_at,
839 source,
840 tool_name: Some(tool_name.to_string()),
841 source_namespace: None,
842 source_tool: None,
843 message: message.into(),
844 session_id: session_id.map(str::to_string),
845 };
846 Self::base(
847 MemoryCategory::Failure,
848 default_importance_score(),
849 String::new(),
850 AinlNodeType::Failure { failure },
851 )
852 }
853
854 pub fn new_ainl_runtime_graph_validation_failure(
857 message: impl Into<String>,
858 session_id: Option<&str>,
859 ) -> Self {
860 let recorded_at = chrono::Utc::now().timestamp();
861 let source = "ainl_runtime:graph_validation".to_string();
862 let failure = FailureNode {
863 recorded_at,
864 source,
865 tool_name: None,
866 source_namespace: None,
867 source_tool: None,
868 message: message.into(),
869 session_id: session_id.map(str::to_string),
870 };
871 Self::base(
872 MemoryCategory::Failure,
873 default_importance_score(),
874 String::new(),
875 AinlNodeType::Failure { failure },
876 )
877 }
878
879 pub fn episodic(&self) -> Option<&EpisodicNode> {
880 match &self.node_type {
881 AinlNodeType::Episode { episodic } => Some(episodic),
882 _ => None,
883 }
884 }
885
886 pub fn semantic(&self) -> Option<&SemanticNode> {
887 match &self.node_type {
888 AinlNodeType::Semantic { semantic } => Some(semantic),
889 _ => None,
890 }
891 }
892
893 pub fn procedural(&self) -> Option<&ProceduralNode> {
894 match &self.node_type {
895 AinlNodeType::Procedural { procedural } => Some(procedural),
896 _ => None,
897 }
898 }
899
900 pub fn persona(&self) -> Option<&PersonaNode> {
901 match &self.node_type {
902 AinlNodeType::Persona { persona } => Some(persona),
903 _ => None,
904 }
905 }
906
907 pub fn trajectory(&self) -> Option<&TrajectoryNode> {
908 match &self.node_type {
909 AinlNodeType::Trajectory { trajectory } => Some(trajectory),
910 _ => None,
911 }
912 }
913
914 pub fn failure(&self) -> Option<&FailureNode> {
915 match &self.node_type {
916 AinlNodeType::Failure { failure } => Some(failure),
917 _ => None,
918 }
919 }
920
921 pub fn add_edge(&mut self, target_id: Uuid, label: impl Into<String>) {
923 self.edges.push(AinlEdge {
924 target_id,
925 label: label.into(),
926 });
927 }
928}
929
930#[cfg(test)]
931mod trajectory_tests {
932 use super::*;
933 use uuid::Uuid;
934
935 #[test]
936 fn trajectory_node_serde_roundtrip() {
937 let traj = TrajectoryNode {
938 episode_id: Uuid::nil(),
939 recorded_at: 1700000000,
940 session_id: "sess".into(),
941 project_id: Some("proj".into()),
942 ainl_source_hash: Some("abc".into()),
943 outcome: TrajectoryOutcome::Success,
944 steps: vec![TrajectoryStep {
945 step_id: "1".into(),
946 timestamp_ms: 1,
947 adapter: "http".into(),
948 operation: "GET".into(),
949 inputs_preview: None,
950 outputs_preview: None,
951 duration_ms: 2,
952 success: true,
953 error: None,
954 vitals: None,
955 freshness_at_step: None,
956 frame_vars: None,
957 tool_telemetry: None,
958 }],
959 duration_ms: 10,
960 frame_vars: None,
961 fitness_delta: None,
962 };
963 let node = AinlMemoryNode {
964 id: Uuid::nil(),
965 memory_category: MemoryCategory::Trajectory,
966 importance_score: 0.5,
967 agent_id: "agent".into(),
968 project_id: None,
969 node_type: AinlNodeType::Trajectory { trajectory: traj },
970 edges: Vec::new(),
971 plugin_data: None,
972 };
973 let json = serde_json::to_string(&node).expect("serialize");
974 let back: AinlMemoryNode = serde_json::from_str(&json).expect("deserialize");
975 assert!(matches!(back.node_type, AinlNodeType::Trajectory { .. }));
976 assert_eq!(back.trajectory().map(|t| t.episode_id), Some(Uuid::nil()));
977 }
978}