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 pub message: String,
437 #[serde(default)]
438 pub session_id: Option<String>,
439}
440
441#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
443#[serde(tag = "type", rename_all = "snake_case")]
444pub enum AinlNodeType {
445 Episode {
447 #[serde(flatten)]
448 episodic: EpisodicNode,
449 },
450
451 Semantic {
453 #[serde(flatten)]
454 semantic: SemanticNode,
455 },
456
457 Procedural {
459 #[serde(flatten)]
460 procedural: ProceduralNode,
461 },
462
463 Persona {
465 #[serde(flatten)]
466 persona: PersonaNode,
467 },
468
469 RuntimeState { runtime_state: RuntimeStateNode },
471
472 Trajectory {
474 trajectory: TrajectoryNode,
475 },
476
477 Failure {
479 failure: FailureNode,
480 },
481}
482
483#[derive(Serialize, Debug, Clone, PartialEq)]
485pub struct AinlMemoryNode {
486 pub id: Uuid,
487 pub memory_category: MemoryCategory,
488 pub importance_score: f32,
489 pub agent_id: String,
490 #[serde(default, skip_serializing_if = "Option::is_none")]
492 pub project_id: Option<String>,
493 pub node_type: AinlNodeType,
494 pub edges: Vec<AinlEdge>,
495}
496
497#[derive(Deserialize)]
498struct AinlMemoryNodeWire {
499 id: Uuid,
500 #[serde(default)]
501 memory_category: Option<MemoryCategory>,
502 #[serde(default)]
503 importance_score: Option<f32>,
504 #[serde(default)]
505 agent_id: Option<String>,
506 #[serde(default)]
507 project_id: Option<String>,
508 node_type: AinlNodeType,
509 #[serde(default)]
510 edges: Vec<AinlEdge>,
511}
512
513impl From<AinlMemoryNodeWire> for AinlMemoryNode {
514 fn from(w: AinlMemoryNodeWire) -> Self {
515 let memory_category = w
516 .memory_category
517 .unwrap_or_else(|| MemoryCategory::from_node_type(&w.node_type));
518 let importance_score = w.importance_score.unwrap_or_else(default_importance_score);
519 Self {
520 id: w.id,
521 memory_category,
522 importance_score,
523 agent_id: w.agent_id.unwrap_or_default(),
524 project_id: w
525 .project_id
526 .as_ref()
527 .map(|s| s.trim().to_string())
528 .filter(|s| !s.is_empty()),
529 node_type: w.node_type,
530 edges: w.edges,
531 }
532 }
533}
534
535impl<'de> Deserialize<'de> for AinlMemoryNode {
536 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
537 where
538 D: serde::Deserializer<'de>,
539 {
540 let w = AinlMemoryNodeWire::deserialize(deserializer)?;
541 Ok(Self::from(w))
542 }
543}
544
545#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
547pub struct AinlEdge {
548 pub target_id: Uuid,
550
551 pub label: String,
553}
554
555impl AinlMemoryNode {
556 fn base(
557 memory_category: MemoryCategory,
558 importance_score: f32,
559 agent_id: String,
560 node_type: AinlNodeType,
561 ) -> Self {
562 Self {
563 id: Uuid::new_v4(),
564 memory_category,
565 importance_score,
566 agent_id,
567 project_id: None,
568 node_type,
569 edges: Vec::new(),
570 }
571 }
572
573 pub fn new_episode(
575 turn_id: Uuid,
576 timestamp: i64,
577 tool_calls: Vec<String>,
578 delegation_to: Option<String>,
579 trace_event: Option<serde_json::Value>,
580 ) -> Self {
581 let tools_invoked = tool_calls.clone();
582 let episodic = EpisodicNode {
583 turn_id,
584 timestamp,
585 tool_calls,
586 delegation_to,
587 trace_event,
588 turn_index: 0,
589 user_message_tokens: 0,
590 assistant_response_tokens: 0,
591 tools_invoked,
592 persona_signals_emitted: Vec::new(),
593 sentiment: None,
594 flagged: false,
595 conversation_id: String::new(),
596 follows_episode_id: None,
597 user_message: None,
598 assistant_response: None,
599 tags: Vec::new(),
600 vitals_gate: None,
601 vitals_phase: None,
602 vitals_trust: None,
603 };
604 Self::base(
605 MemoryCategory::Episodic,
606 default_importance_score(),
607 String::new(),
608 AinlNodeType::Episode { episodic },
609 )
610 }
611
612 pub fn new_fact(fact: String, confidence: f32, source_turn_id: Uuid) -> Self {
614 let semantic = SemanticNode {
615 fact,
616 confidence,
617 source_turn_id,
618 topic_cluster: None,
619 source_episode_id: String::new(),
620 contradiction_ids: Vec::new(),
621 last_referenced_at: 0,
622 reference_count: 0,
623 decay_eligible: true,
624 tags: Vec::new(),
625 recurrence_count: 0,
626 last_ref_snapshot: 0,
627 };
628 Self::base(
629 MemoryCategory::Semantic,
630 default_importance_score(),
631 String::new(),
632 AinlNodeType::Semantic { semantic },
633 )
634 }
635
636 pub fn new_pattern(pattern_name: String, compiled_graph: Vec<u8>) -> Self {
638 let mut procedural = ProceduralNode {
639 pattern_name,
640 compiled_graph,
641 tool_sequence: Vec::new(),
642 confidence: None,
643 procedure_type: ProcedureType::default(),
644 trigger_conditions: Vec::new(),
645 success_count: 0,
646 failure_count: 0,
647 success_rate: default_success_rate(),
648 last_invoked_at: 0,
649 reinforcement_episode_ids: Vec::new(),
650 suppression_episode_ids: Vec::new(),
651 patch_version: 1,
652 fitness: None,
653 declared_reads: Vec::new(),
654 retired: false,
655 label: String::new(),
656 trace_id: None,
657 pattern_observation_count: 0,
658 prompt_eligible: true,
659 };
660 procedural.recompute_success_rate();
661 Self::base(
662 MemoryCategory::Procedural,
663 default_importance_score(),
664 String::new(),
665 AinlNodeType::Procedural { procedural },
666 )
667 }
668
669 pub fn new_procedural_tools(
671 pattern_name: String,
672 tool_sequence: Vec<String>,
673 confidence: f32,
674 ) -> Self {
675 use crate::pattern_promotion;
676 let c = confidence.clamp(0.0, 1.0);
677 let ema0 = pattern_promotion::ema_fitness_update(None, c);
678 let mut procedural = ProceduralNode {
679 pattern_name,
680 compiled_graph: Vec::new(),
681 tool_sequence,
682 confidence: Some(c),
683 procedure_type: ProcedureType::ToolSequence,
684 trigger_conditions: Vec::new(),
685 success_count: 0,
686 failure_count: 0,
687 success_rate: default_success_rate(),
688 last_invoked_at: 0,
689 reinforcement_episode_ids: Vec::new(),
690 suppression_episode_ids: Vec::new(),
691 patch_version: 1,
692 fitness: Some(ema0),
693 declared_reads: Vec::new(),
694 retired: false,
695 label: String::new(),
696 trace_id: None,
697 pattern_observation_count: 1,
698 prompt_eligible: false,
699 };
700 procedural.recompute_success_rate();
701 Self::base(
702 MemoryCategory::Procedural,
703 default_importance_score(),
704 String::new(),
705 AinlNodeType::Procedural { procedural },
706 )
707 }
708
709 pub fn new_persona(trait_name: String, strength: f32, learned_from: Vec<Uuid>) -> Self {
711 let persona = PersonaNode {
712 trait_name,
713 strength,
714 learned_from,
715 layer: PersonaLayer::default(),
716 source: PersonaSource::default(),
717 strength_floor: default_strength_floor(),
718 locked: false,
719 relevance_score: 0.0,
720 provenance_episode_ids: Vec::new(),
721 evolution_log: Vec::new(),
722 axis_scores: HashMap::new(),
723 evolution_cycle: 0,
724 last_evolved: String::new(),
725 agent_id: String::new(),
726 dominant_axes: Vec::new(),
727 };
728 Self::base(
729 MemoryCategory::Persona,
730 default_importance_score(),
731 String::new(),
732 AinlNodeType::Persona { persona },
733 )
734 }
735
736 pub fn new_trajectory(trajectory: TrajectoryNode, agent_id: impl Into<String>) -> Self {
738 Self::base(
739 MemoryCategory::Trajectory,
740 default_importance_score(),
741 agent_id.into(),
742 AinlNodeType::Trajectory { trajectory },
743 )
744 }
745
746 pub fn new_loop_guard_failure(
748 verdict_label: &str,
749 tool_name: Option<&str>,
750 message: impl Into<String>,
751 session_id: Option<&str>,
752 ) -> Self {
753 let recorded_at = chrono::Utc::now().timestamp();
754 let source = format!("loop_guard:{verdict_label}");
755 let failure = FailureNode {
756 recorded_at,
757 source,
758 tool_name: tool_name.map(str::to_string),
759 message: message.into(),
760 session_id: session_id.map(str::to_string),
761 };
762 Self::base(
763 MemoryCategory::Failure,
764 default_importance_score(),
765 String::new(),
766 AinlNodeType::Failure { failure },
767 )
768 }
769
770 pub fn new_tool_execution_failure(
774 tool_name: &str,
775 message: impl Into<String>,
776 session_id: Option<&str>,
777 ) -> Self {
778 let recorded_at = chrono::Utc::now().timestamp();
779 let source = "tool_runner:error".to_string();
780 let failure = FailureNode {
781 recorded_at,
782 source,
783 tool_name: Some(tool_name.to_string()),
784 message: message.into(),
785 session_id: session_id.map(str::to_string),
786 };
787 Self::base(
788 MemoryCategory::Failure,
789 default_importance_score(),
790 String::new(),
791 AinlNodeType::Failure { failure },
792 )
793 }
794
795 pub fn new_agent_loop_precheck_failure(
799 kind: &str,
800 tool_name: &str,
801 message: impl Into<String>,
802 session_id: Option<&str>,
803 ) -> Self {
804 let recorded_at = chrono::Utc::now().timestamp();
805 let source = format!("agent_loop:{kind}");
806 let failure = FailureNode {
807 recorded_at,
808 source,
809 tool_name: Some(tool_name.to_string()),
810 message: message.into(),
811 session_id: session_id.map(str::to_string),
812 };
813 Self::base(
814 MemoryCategory::Failure,
815 default_importance_score(),
816 String::new(),
817 AinlNodeType::Failure { failure },
818 )
819 }
820
821 pub fn new_ainl_runtime_graph_validation_failure(
824 message: impl Into<String>,
825 session_id: Option<&str>,
826 ) -> Self {
827 let recorded_at = chrono::Utc::now().timestamp();
828 let source = "ainl_runtime:graph_validation".to_string();
829 let failure = FailureNode {
830 recorded_at,
831 source,
832 tool_name: None,
833 message: message.into(),
834 session_id: session_id.map(str::to_string),
835 };
836 Self::base(
837 MemoryCategory::Failure,
838 default_importance_score(),
839 String::new(),
840 AinlNodeType::Failure { failure },
841 )
842 }
843
844 pub fn episodic(&self) -> Option<&EpisodicNode> {
845 match &self.node_type {
846 AinlNodeType::Episode { episodic } => Some(episodic),
847 _ => None,
848 }
849 }
850
851 pub fn semantic(&self) -> Option<&SemanticNode> {
852 match &self.node_type {
853 AinlNodeType::Semantic { semantic } => Some(semantic),
854 _ => None,
855 }
856 }
857
858 pub fn procedural(&self) -> Option<&ProceduralNode> {
859 match &self.node_type {
860 AinlNodeType::Procedural { procedural } => Some(procedural),
861 _ => None,
862 }
863 }
864
865 pub fn persona(&self) -> Option<&PersonaNode> {
866 match &self.node_type {
867 AinlNodeType::Persona { persona } => Some(persona),
868 _ => None,
869 }
870 }
871
872 pub fn trajectory(&self) -> Option<&TrajectoryNode> {
873 match &self.node_type {
874 AinlNodeType::Trajectory { trajectory } => Some(trajectory),
875 _ => None,
876 }
877 }
878
879 pub fn failure(&self) -> Option<&FailureNode> {
880 match &self.node_type {
881 AinlNodeType::Failure { failure } => Some(failure),
882 _ => None,
883 }
884 }
885
886 pub fn add_edge(&mut self, target_id: Uuid, label: impl Into<String>) {
888 self.edges.push(AinlEdge {
889 target_id,
890 label: label.into(),
891 });
892 }
893}
894
895#[cfg(test)]
896mod trajectory_tests {
897 use super::*;
898 use uuid::Uuid;
899
900 #[test]
901 fn trajectory_node_serde_roundtrip() {
902 let traj = TrajectoryNode {
903 episode_id: Uuid::nil(),
904 recorded_at: 1700000000,
905 session_id: "sess".into(),
906 project_id: Some("proj".into()),
907 ainl_source_hash: Some("abc".into()),
908 outcome: TrajectoryOutcome::Success,
909 steps: vec![TrajectoryStep {
910 step_id: "1".into(),
911 timestamp_ms: 1,
912 adapter: "http".into(),
913 operation: "GET".into(),
914 inputs_preview: None,
915 outputs_preview: None,
916 duration_ms: 2,
917 success: true,
918 error: None,
919 vitals: None,
920 freshness_at_step: None,
921 frame_vars: None,
922 tool_telemetry: None,
923 }],
924 duration_ms: 10,
925 frame_vars: None,
926 fitness_delta: None,
927 };
928 let node = AinlMemoryNode {
929 id: Uuid::nil(),
930 memory_category: MemoryCategory::Trajectory,
931 importance_score: 0.5,
932 agent_id: "agent".into(),
933 project_id: None,
934 node_type: AinlNodeType::Trajectory { trajectory: traj },
935 edges: Vec::new(),
936 };
937 let json = serde_json::to_string(&node).expect("serialize");
938 let back: AinlMemoryNode = serde_json::from_str(&json).expect("deserialize");
939 assert!(matches!(
940 back.node_type,
941 AinlNodeType::Trajectory { .. }
942 ));
943 assert_eq!(
944 back.trajectory().map(|t| t.episode_id),
945 Some(Uuid::nil())
946 );
947 }
948}