1use ainl_contracts::{
9 Assertion, Feature, Handoff, Mission, TrajectoryOutcome, TrajectoryStep,
10};
11use serde::{Deserialize, Deserializer, Serialize};
12use std::collections::HashMap;
13use std::fmt;
14use uuid::Uuid;
15
16fn deserialize_updated_at<'de, D>(deserializer: D) -> Result<i64, D::Error>
17where
18 D: Deserializer<'de>,
19{
20 use serde::de::{self, Visitor};
21
22 struct TsVisitor;
23 impl<'de> Visitor<'de> for TsVisitor {
24 type Value = i64;
25
26 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
27 f.write_str("unix timestamp or RFC3339 string")
28 }
29
30 fn visit_i64<E: de::Error>(self, v: i64) -> Result<i64, E> {
31 Ok(v)
32 }
33
34 fn visit_u64<E: de::Error>(self, v: u64) -> Result<i64, E> {
35 i64::try_from(v).map_err(de::Error::custom)
36 }
37
38 fn visit_str<E: de::Error>(self, v: &str) -> Result<i64, E> {
39 if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(v) {
40 return Ok(dt.timestamp());
41 }
42 v.parse::<i64>().map_err(de::Error::custom)
43 }
44
45 fn visit_string<E: de::Error>(self, v: String) -> Result<i64, E> {
46 self.visit_str(&v)
47 }
48 }
49
50 deserializer.deserialize_any(TsVisitor)
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum AinlNodeKind {
56 Episode,
57 Semantic,
58 Procedural,
59 Persona,
60 RuntimeState,
62 Trajectory,
64 Failure,
66 Mission,
68 Feature,
70 Assertion,
72 Handoff,
74 Extension,
76}
77
78impl AinlNodeKind {
79 pub fn as_str(&self) -> &'static str {
80 match self {
81 Self::Episode => "episode",
82 Self::Semantic => "semantic",
83 Self::Procedural => "procedural",
84 Self::Persona => "persona",
85 Self::RuntimeState => "runtime_state",
86 Self::Trajectory => "trajectory",
87 Self::Failure => "failure",
88 Self::Mission => "mission",
89 Self::Feature => "feature",
90 Self::Assertion => "assertion",
91 Self::Handoff => "handoff",
92 Self::Extension => "extension",
93 }
94 }
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
99#[serde(rename_all = "snake_case")]
100pub enum MemoryCategory {
101 Persona,
102 Semantic,
103 Episodic,
104 Procedural,
105 RuntimeState,
107 Trajectory,
109 Failure,
111 Mission,
113 Feature,
115 Assertion,
117 Handoff,
119 Extension,
121}
122
123impl MemoryCategory {
124 pub fn from_node_type(node_type: &AinlNodeType) -> Self {
125 match node_type {
126 AinlNodeType::Episode { .. } => MemoryCategory::Episodic,
127 AinlNodeType::Semantic { .. } => MemoryCategory::Semantic,
128 AinlNodeType::Procedural { .. } => MemoryCategory::Procedural,
129 AinlNodeType::Persona { .. } => MemoryCategory::Persona,
130 AinlNodeType::RuntimeState { .. } => MemoryCategory::RuntimeState,
131 AinlNodeType::Trajectory { .. } => MemoryCategory::Trajectory,
132 AinlNodeType::Failure { .. } => MemoryCategory::Failure,
133 AinlNodeType::Mission { .. } => MemoryCategory::Mission,
134 AinlNodeType::Feature { .. } => MemoryCategory::Feature,
135 AinlNodeType::Assertion { .. } => MemoryCategory::Assertion,
136 AinlNodeType::Handoff { .. } => MemoryCategory::Handoff,
137 AinlNodeType::Extension { .. } => MemoryCategory::Extension,
138 }
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
143#[serde(rename_all = "snake_case")]
144pub enum PersonaLayer {
145 #[default]
146 Base,
147 Delta,
148 Injection,
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
152#[serde(rename_all = "snake_case")]
153pub enum PersonaSource {
154 SystemDefault,
155 #[default]
156 UserConfigured,
157 Evolved,
158 Feedback,
159 Injection,
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(rename_all = "snake_case")]
164pub enum Sentiment {
165 Positive,
166 Neutral,
167 Negative,
168 Mixed,
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
172#[serde(rename_all = "snake_case")]
173pub enum ProcedureType {
174 #[default]
175 ToolSequence,
176 ResponsePattern,
177 WorkflowStep,
178 BehavioralRule,
179}
180
181#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
183pub struct StrengthEvent {
184 pub delta: f32,
185 pub reason: String,
186 pub episode_id: String,
187 pub timestamp: u64,
188}
189
190fn default_importance_score() -> f32 {
191 0.5
192}
193
194fn default_semantic_confidence() -> f32 {
195 0.7
196}
197
198fn default_decay_eligible() -> bool {
199 true
200}
201
202fn default_success_rate() -> f32 {
203 0.5
204}
205
206fn default_procedural_prompt_eligible() -> bool {
207 true
208}
209
210fn default_strength_floor() -> f32 {
211 0.0
212}
213
214#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
216pub struct PersonaNode {
217 pub trait_name: String,
218 pub strength: f32,
219 #[serde(default)]
220 pub learned_from: Vec<Uuid>,
221 #[serde(default)]
222 pub layer: PersonaLayer,
223 #[serde(default)]
224 pub source: PersonaSource,
225 #[serde(default = "default_strength_floor")]
226 pub strength_floor: f32,
227 #[serde(default)]
228 pub locked: bool,
229 #[serde(default)]
230 pub relevance_score: f32,
231 #[serde(default)]
232 pub provenance_episode_ids: Vec<String>,
233 #[serde(default)]
234 pub evolution_log: Vec<StrengthEvent>,
235 #[serde(default)]
237 pub axis_scores: HashMap<String, f32>,
238 #[serde(default)]
239 pub evolution_cycle: u32,
240 #[serde(default)]
242 pub last_evolved: String,
243 #[serde(default)]
245 pub agent_id: String,
246 #[serde(default)]
248 pub dominant_axes: Vec<String>,
249}
250
251#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
253pub struct SemanticNode {
254 pub fact: String,
255 #[serde(default = "default_semantic_confidence")]
256 pub confidence: f32,
257 pub source_turn_id: Uuid,
258 #[serde(default)]
259 pub topic_cluster: Option<String>,
260 #[serde(default)]
261 pub source_episode_id: String,
262 #[serde(default)]
263 pub contradiction_ids: Vec<String>,
264 #[serde(default)]
265 pub last_referenced_at: u64,
266 #[serde(default)]
269 pub reference_count: u32,
270 #[serde(default = "default_decay_eligible")]
271 pub decay_eligible: bool,
272 #[serde(default)]
274 pub tags: Vec<String>,
275 #[serde(default)]
282 pub recurrence_count: u32,
283 #[serde(rename = "_last_ref_snapshot", default)]
285 pub last_ref_snapshot: u32,
286}
287
288#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
290pub struct EpisodicNode {
291 pub turn_id: Uuid,
292 pub timestamp: i64,
293 #[serde(default)]
294 pub tool_calls: Vec<String>,
295 #[serde(default)]
296 pub delegation_to: Option<String>,
297 #[serde(skip_serializing_if = "Option::is_none")]
298 pub trace_event: Option<serde_json::Value>,
299 #[serde(default)]
300 pub turn_index: u32,
301 #[serde(default)]
302 pub user_message_tokens: u32,
303 #[serde(default)]
304 pub assistant_response_tokens: u32,
305 #[serde(default)]
307 pub tools_invoked: Vec<String>,
308 #[serde(default)]
311 pub persona_signals_emitted: Vec<String>,
312 #[serde(default)]
313 pub sentiment: Option<Sentiment>,
314 #[serde(default)]
315 pub flagged: bool,
316 #[serde(default)]
317 pub conversation_id: String,
318 #[serde(default)]
319 pub follows_episode_id: Option<String>,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
322 pub user_message: Option<String>,
323 #[serde(default, skip_serializing_if = "Option::is_none")]
325 pub assistant_response: Option<String>,
326 #[serde(default, skip_serializing_if = "Vec::is_empty")]
328 pub tags: Vec<String>,
329 #[serde(default, skip_serializing_if = "Option::is_none")]
332 pub vitals_gate: Option<String>,
333 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub vitals_phase: Option<String>,
336 #[serde(default, skip_serializing_if = "Option::is_none")]
338 pub vitals_trust: Option<f32>,
339}
340
341impl EpisodicNode {
342 pub fn effective_tools(&self) -> &[String] {
344 if !self.tools_invoked.is_empty() {
345 &self.tools_invoked
346 } else {
347 &self.tool_calls
348 }
349 }
350}
351
352#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
354pub struct ProceduralNode {
355 pub pattern_name: String,
356 #[serde(default)]
357 pub compiled_graph: Vec<u8>,
358 #[serde(default, skip_serializing_if = "Vec::is_empty")]
359 pub tool_sequence: Vec<String>,
360 #[serde(default, skip_serializing_if = "Option::is_none")]
361 pub confidence: Option<f32>,
362 #[serde(default)]
363 pub procedure_type: ProcedureType,
364 #[serde(default)]
365 pub trigger_conditions: Vec<String>,
366 #[serde(default)]
367 pub success_count: u32,
368 #[serde(default)]
369 pub failure_count: u32,
370 #[serde(default = "default_success_rate")]
371 pub success_rate: f32,
372 #[serde(default)]
373 pub last_invoked_at: u64,
374 #[serde(default)]
375 pub reinforcement_episode_ids: Vec<String>,
376 #[serde(default)]
377 pub suppression_episode_ids: Vec<String>,
378 #[serde(default)]
380 pub patch_version: u32,
381 #[serde(default)]
383 pub fitness: Option<f32>,
384 #[serde(default)]
386 pub declared_reads: Vec<String>,
387 #[serde(default)]
389 pub retired: bool,
390 #[serde(default)]
392 pub label: String,
393 #[serde(default, skip_serializing_if = "Option::is_none")]
395 pub trace_id: Option<String>,
396 #[serde(default)]
398 pub pattern_observation_count: u32,
399 #[serde(default = "default_procedural_prompt_eligible")]
403 pub prompt_eligible: bool,
404}
405
406impl ProceduralNode {
407 pub fn recompute_success_rate(&mut self) {
408 let total = self.success_count.saturating_add(self.failure_count);
409 self.success_rate = if total == 0 {
410 0.5
411 } else {
412 self.success_count as f32 / total as f32
413 };
414 }
415}
416
417#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
420pub struct RuntimeStateNode {
421 pub agent_id: String,
422 #[serde(default)]
423 pub turn_count: u64,
424 #[serde(default, alias = "last_extraction_turn")]
425 pub last_extraction_at_turn: u64,
426 #[serde(default, alias = "last_persona_prompt")]
428 pub persona_snapshot_json: Option<String>,
429 #[serde(default, deserialize_with = "deserialize_updated_at")]
430 pub updated_at: i64,
431}
432
433#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
435pub struct TrajectoryNode {
436 pub episode_id: Uuid,
438 pub recorded_at: i64,
440 #[serde(default)]
441 pub session_id: String,
442 #[serde(default)]
443 pub project_id: Option<String>,
444 #[serde(default)]
445 pub ainl_source_hash: Option<String>,
446 pub outcome: TrajectoryOutcome,
447 #[serde(default)]
448 pub steps: Vec<TrajectoryStep>,
449 #[serde(default)]
450 pub duration_ms: u64,
451 #[serde(default, skip_serializing_if = "Option::is_none")]
453 pub frame_vars: Option<serde_json::Value>,
454 #[serde(default, skip_serializing_if = "Option::is_none")]
456 pub fitness_delta: Option<f32>,
457}
458
459#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
461pub struct FailureNode {
462 pub recorded_at: i64,
464 pub source: String,
466 #[serde(default)]
467 pub tool_name: Option<String>,
468 #[serde(default, skip_serializing_if = "Option::is_none")]
470 pub source_namespace: Option<String>,
471 #[serde(default, skip_serializing_if = "Option::is_none")]
473 pub source_tool: Option<String>,
474 pub message: String,
475 #[serde(default)]
476 pub session_id: Option<String>,
477}
478
479#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
481#[serde(tag = "type", rename_all = "snake_case")]
482pub enum AinlNodeType {
483 Episode {
485 #[serde(flatten)]
486 episodic: EpisodicNode,
487 },
488
489 Semantic {
491 #[serde(flatten)]
492 semantic: SemanticNode,
493 },
494
495 Procedural {
497 #[serde(flatten)]
498 procedural: ProceduralNode,
499 },
500
501 Persona {
503 #[serde(flatten)]
504 persona: PersonaNode,
505 },
506
507 RuntimeState { runtime_state: RuntimeStateNode },
509
510 Trajectory { trajectory: TrajectoryNode },
512
513 Failure { failure: FailureNode },
515
516 Mission { mission: Mission },
518
519 Feature { feature: Feature },
521
522 Assertion { assertion: Assertion },
524
525 Handoff { handoff: Handoff },
527
528 Extension {
530 kind: String,
531 schema_version: u32,
532 payload: serde_json::Value,
533 },
534}
535
536impl AinlNodeType {
537 pub fn extension_from_typed<T: Serialize>(
539 kind: impl Into<String>,
540 schema_version: u32,
541 value: &T,
542 ) -> Result<Self, serde_json::Error> {
543 Ok(Self::Extension {
544 kind: kind.into(),
545 schema_version,
546 payload: serde_json::to_value(value)?,
547 })
548 }
549
550 pub fn extension_downcast<T: for<'de> Deserialize<'de>>(&self) -> Option<T> {
552 match self {
553 Self::Extension { payload, .. } => serde_json::from_value(payload.clone()).ok(),
554 _ => None,
555 }
556 }
557}
558
559#[derive(Serialize, Debug, Clone, PartialEq)]
561pub struct AinlMemoryNode {
562 pub id: Uuid,
563 pub memory_category: MemoryCategory,
564 pub importance_score: f32,
565 pub agent_id: String,
566 #[serde(default, skip_serializing_if = "Option::is_none")]
568 pub project_id: Option<String>,
569 pub node_type: AinlNodeType,
570 pub edges: Vec<AinlEdge>,
571 #[serde(default, skip_serializing_if = "Option::is_none")]
574 pub plugin_data: Option<serde_json::Value>,
575}
576
577#[derive(Deserialize)]
578struct AinlMemoryNodeWire {
579 id: Uuid,
580 #[serde(default)]
581 memory_category: Option<MemoryCategory>,
582 #[serde(default)]
583 importance_score: Option<f32>,
584 #[serde(default)]
585 agent_id: Option<String>,
586 #[serde(default)]
587 project_id: Option<String>,
588 node_type: AinlNodeType,
589 #[serde(default)]
590 edges: Vec<AinlEdge>,
591 #[serde(default)]
592 plugin_data: Option<serde_json::Value>,
593}
594
595impl From<AinlMemoryNodeWire> for AinlMemoryNode {
596 fn from(w: AinlMemoryNodeWire) -> Self {
597 let memory_category = w
598 .memory_category
599 .unwrap_or_else(|| MemoryCategory::from_node_type(&w.node_type));
600 let importance_score = w.importance_score.unwrap_or_else(default_importance_score);
601 Self {
602 id: w.id,
603 memory_category,
604 importance_score,
605 agent_id: w.agent_id.unwrap_or_default(),
606 project_id: w
607 .project_id
608 .as_ref()
609 .map(|s| s.trim().to_string())
610 .filter(|s| !s.is_empty()),
611 node_type: w.node_type,
612 edges: w.edges,
613 plugin_data: w.plugin_data,
614 }
615 }
616}
617
618impl<'de> Deserialize<'de> for AinlMemoryNode {
619 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
620 where
621 D: serde::Deserializer<'de>,
622 {
623 let w = AinlMemoryNodeWire::deserialize(deserializer)?;
624 Ok(Self::from(w))
625 }
626}
627
628#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
630pub struct AinlEdge {
631 pub target_id: Uuid,
633
634 pub label: String,
636}
637
638impl AinlMemoryNode {
639 fn base(
640 memory_category: MemoryCategory,
641 importance_score: f32,
642 agent_id: String,
643 node_type: AinlNodeType,
644 ) -> Self {
645 Self {
646 id: Uuid::new_v4(),
647 memory_category,
648 importance_score,
649 agent_id,
650 project_id: None,
651 node_type,
652 edges: Vec::new(),
653 plugin_data: None,
654 }
655 }
656
657 pub fn new_episode(
659 turn_id: Uuid,
660 timestamp: i64,
661 tool_calls: Vec<String>,
662 delegation_to: Option<String>,
663 trace_event: Option<serde_json::Value>,
664 ) -> Self {
665 let tools_invoked = tool_calls.clone();
666 let episodic = EpisodicNode {
667 turn_id,
668 timestamp,
669 tool_calls,
670 delegation_to,
671 trace_event,
672 turn_index: 0,
673 user_message_tokens: 0,
674 assistant_response_tokens: 0,
675 tools_invoked,
676 persona_signals_emitted: Vec::new(),
677 sentiment: None,
678 flagged: false,
679 conversation_id: String::new(),
680 follows_episode_id: None,
681 user_message: None,
682 assistant_response: None,
683 tags: Vec::new(),
684 vitals_gate: None,
685 vitals_phase: None,
686 vitals_trust: None,
687 };
688 Self::base(
689 MemoryCategory::Episodic,
690 default_importance_score(),
691 String::new(),
692 AinlNodeType::Episode { episodic },
693 )
694 }
695
696 pub fn new_fact(fact: String, confidence: f32, source_turn_id: Uuid) -> Self {
698 let semantic = SemanticNode {
699 fact,
700 confidence,
701 source_turn_id,
702 topic_cluster: None,
703 source_episode_id: String::new(),
704 contradiction_ids: Vec::new(),
705 last_referenced_at: 0,
706 reference_count: 0,
707 decay_eligible: true,
708 tags: Vec::new(),
709 recurrence_count: 0,
710 last_ref_snapshot: 0,
711 };
712 Self::base(
713 MemoryCategory::Semantic,
714 default_importance_score(),
715 String::new(),
716 AinlNodeType::Semantic { semantic },
717 )
718 }
719
720 pub fn new_pattern(pattern_name: String, compiled_graph: Vec<u8>) -> Self {
722 let mut procedural = ProceduralNode {
723 pattern_name,
724 compiled_graph,
725 tool_sequence: Vec::new(),
726 confidence: None,
727 procedure_type: ProcedureType::default(),
728 trigger_conditions: Vec::new(),
729 success_count: 0,
730 failure_count: 0,
731 success_rate: default_success_rate(),
732 last_invoked_at: 0,
733 reinforcement_episode_ids: Vec::new(),
734 suppression_episode_ids: Vec::new(),
735 patch_version: 1,
736 fitness: None,
737 declared_reads: Vec::new(),
738 retired: false,
739 label: String::new(),
740 trace_id: None,
741 pattern_observation_count: 0,
742 prompt_eligible: true,
743 };
744 procedural.recompute_success_rate();
745 Self::base(
746 MemoryCategory::Procedural,
747 default_importance_score(),
748 String::new(),
749 AinlNodeType::Procedural { procedural },
750 )
751 }
752
753 pub fn new_procedural_tools(
755 pattern_name: String,
756 tool_sequence: Vec<String>,
757 confidence: f32,
758 ) -> Self {
759 use crate::pattern_promotion;
760 let c = confidence.clamp(0.0, 1.0);
761 let ema0 = pattern_promotion::ema_fitness_update(None, c);
762 let mut procedural = ProceduralNode {
763 pattern_name,
764 compiled_graph: Vec::new(),
765 tool_sequence,
766 confidence: Some(c),
767 procedure_type: ProcedureType::ToolSequence,
768 trigger_conditions: Vec::new(),
769 success_count: 0,
770 failure_count: 0,
771 success_rate: default_success_rate(),
772 last_invoked_at: 0,
773 reinforcement_episode_ids: Vec::new(),
774 suppression_episode_ids: Vec::new(),
775 patch_version: 1,
776 fitness: Some(ema0),
777 declared_reads: Vec::new(),
778 retired: false,
779 label: String::new(),
780 trace_id: None,
781 pattern_observation_count: 1,
782 prompt_eligible: false,
783 };
784 procedural.recompute_success_rate();
785 Self::base(
786 MemoryCategory::Procedural,
787 default_importance_score(),
788 String::new(),
789 AinlNodeType::Procedural { procedural },
790 )
791 }
792
793 pub fn new_persona(trait_name: String, strength: f32, learned_from: Vec<Uuid>) -> Self {
795 let persona = PersonaNode {
796 trait_name,
797 strength,
798 learned_from,
799 layer: PersonaLayer::default(),
800 source: PersonaSource::default(),
801 strength_floor: default_strength_floor(),
802 locked: false,
803 relevance_score: 0.0,
804 provenance_episode_ids: Vec::new(),
805 evolution_log: Vec::new(),
806 axis_scores: HashMap::new(),
807 evolution_cycle: 0,
808 last_evolved: String::new(),
809 agent_id: String::new(),
810 dominant_axes: Vec::new(),
811 };
812 Self::base(
813 MemoryCategory::Persona,
814 default_importance_score(),
815 String::new(),
816 AinlNodeType::Persona { persona },
817 )
818 }
819
820 pub fn new_trajectory(trajectory: TrajectoryNode, agent_id: impl Into<String>) -> Self {
822 Self::base(
823 MemoryCategory::Trajectory,
824 default_importance_score(),
825 agent_id.into(),
826 AinlNodeType::Trajectory { trajectory },
827 )
828 }
829
830 pub fn new_loop_guard_failure(
832 verdict_label: &str,
833 tool_name: Option<&str>,
834 message: impl Into<String>,
835 session_id: Option<&str>,
836 ) -> Self {
837 let recorded_at = chrono::Utc::now().timestamp();
838 let source = format!("loop_guard:{verdict_label}");
839 let failure = FailureNode {
840 recorded_at,
841 source,
842 tool_name: tool_name.map(str::to_string),
843 source_namespace: None,
844 source_tool: None,
845 message: message.into(),
846 session_id: session_id.map(str::to_string),
847 };
848 Self::base(
849 MemoryCategory::Failure,
850 default_importance_score(),
851 String::new(),
852 AinlNodeType::Failure { failure },
853 )
854 }
855
856 pub fn new_tool_execution_failure(
860 tool_name: &str,
861 message: impl Into<String>,
862 session_id: Option<&str>,
863 ) -> Self {
864 Self::new_tool_execution_failure_with_source(
865 tool_name,
866 message,
867 session_id,
868 None::<&str>,
869 None::<&str>,
870 )
871 }
872
873 pub fn new_tool_execution_failure_with_source(
875 tool_name: &str,
876 message: impl Into<String>,
877 session_id: Option<&str>,
878 source_namespace: Option<&str>,
879 source_tool: Option<&str>,
880 ) -> Self {
881 let recorded_at = chrono::Utc::now().timestamp();
882 let source = "tool_runner:error".to_string();
883 let failure = FailureNode {
884 recorded_at,
885 source,
886 tool_name: Some(tool_name.to_string()),
887 source_namespace: source_namespace.map(str::to_string),
888 source_tool: source_tool.map(str::to_string),
889 message: message.into(),
890 session_id: session_id.map(str::to_string),
891 };
892 Self::base(
893 MemoryCategory::Failure,
894 default_importance_score(),
895 String::new(),
896 AinlNodeType::Failure { failure },
897 )
898 }
899
900 pub fn new_agent_loop_precheck_failure(
904 kind: &str,
905 tool_name: &str,
906 message: impl Into<String>,
907 session_id: Option<&str>,
908 ) -> Self {
909 let recorded_at = chrono::Utc::now().timestamp();
910 let source = format!("agent_loop:{kind}");
911 let failure = FailureNode {
912 recorded_at,
913 source,
914 tool_name: Some(tool_name.to_string()),
915 source_namespace: None,
916 source_tool: None,
917 message: message.into(),
918 session_id: session_id.map(str::to_string),
919 };
920 Self::base(
921 MemoryCategory::Failure,
922 default_importance_score(),
923 String::new(),
924 AinlNodeType::Failure { failure },
925 )
926 }
927
928 pub fn new_ainl_runtime_graph_validation_failure(
931 message: impl Into<String>,
932 session_id: Option<&str>,
933 ) -> Self {
934 let recorded_at = chrono::Utc::now().timestamp();
935 let source = "ainl_runtime:graph_validation".to_string();
936 let failure = FailureNode {
937 recorded_at,
938 source,
939 tool_name: None,
940 source_namespace: None,
941 source_tool: None,
942 message: message.into(),
943 session_id: session_id.map(str::to_string),
944 };
945 Self::base(
946 MemoryCategory::Failure,
947 default_importance_score(),
948 String::new(),
949 AinlNodeType::Failure { failure },
950 )
951 }
952
953 pub fn episodic(&self) -> Option<&EpisodicNode> {
954 match &self.node_type {
955 AinlNodeType::Episode { episodic } => Some(episodic),
956 _ => None,
957 }
958 }
959
960 pub fn semantic(&self) -> Option<&SemanticNode> {
961 match &self.node_type {
962 AinlNodeType::Semantic { semantic } => Some(semantic),
963 _ => None,
964 }
965 }
966
967 pub fn procedural(&self) -> Option<&ProceduralNode> {
968 match &self.node_type {
969 AinlNodeType::Procedural { procedural } => Some(procedural),
970 _ => None,
971 }
972 }
973
974 pub fn persona(&self) -> Option<&PersonaNode> {
975 match &self.node_type {
976 AinlNodeType::Persona { persona } => Some(persona),
977 _ => None,
978 }
979 }
980
981 pub fn trajectory(&self) -> Option<&TrajectoryNode> {
982 match &self.node_type {
983 AinlNodeType::Trajectory { trajectory } => Some(trajectory),
984 _ => None,
985 }
986 }
987
988 pub fn failure(&self) -> Option<&FailureNode> {
989 match &self.node_type {
990 AinlNodeType::Failure { failure } => Some(failure),
991 _ => None,
992 }
993 }
994
995 pub fn mission(&self) -> Option<&Mission> {
996 match &self.node_type {
997 AinlNodeType::Mission { mission } => Some(mission),
998 _ => None,
999 }
1000 }
1001
1002 pub fn feature(&self) -> Option<&Feature> {
1003 match &self.node_type {
1004 AinlNodeType::Feature { feature } => Some(feature),
1005 _ => None,
1006 }
1007 }
1008
1009 pub fn assertion(&self) -> Option<&Assertion> {
1010 match &self.node_type {
1011 AinlNodeType::Assertion { assertion } => Some(assertion),
1012 _ => None,
1013 }
1014 }
1015
1016 pub fn handoff(&self) -> Option<&Handoff> {
1017 match &self.node_type {
1018 AinlNodeType::Handoff { handoff } => Some(handoff),
1019 _ => None,
1020 }
1021 }
1022
1023 pub fn mission_id_key(&self) -> Option<&str> {
1025 match &self.node_type {
1026 AinlNodeType::Mission { mission } => Some(mission.mission_id.as_str()),
1027 _ => None,
1028 }
1029 }
1030
1031 pub fn feature_id_key(&self) -> Option<&str> {
1033 match &self.node_type {
1034 AinlNodeType::Feature { feature } => Some(feature.feature_id.as_str()),
1035 _ => None,
1036 }
1037 }
1038
1039 pub fn add_edge(&mut self, target_id: Uuid, label: impl Into<String>) {
1041 self.edges.push(AinlEdge {
1042 target_id,
1043 label: label.into(),
1044 });
1045 }
1046}
1047
1048#[cfg(test)]
1049mod trajectory_tests {
1050 use super::*;
1051 use uuid::Uuid;
1052
1053 #[test]
1054 fn trajectory_node_serde_roundtrip() {
1055 let traj = TrajectoryNode {
1056 episode_id: Uuid::nil(),
1057 recorded_at: 1700000000,
1058 session_id: "sess".into(),
1059 project_id: Some("proj".into()),
1060 ainl_source_hash: Some("abc".into()),
1061 outcome: TrajectoryOutcome::Success,
1062 steps: vec![TrajectoryStep {
1063 step_id: "1".into(),
1064 timestamp_ms: 1,
1065 adapter: "http".into(),
1066 operation: "GET".into(),
1067 inputs_preview: None,
1068 outputs_preview: None,
1069 duration_ms: 2,
1070 success: true,
1071 error: None,
1072 vitals: None,
1073 freshness_at_step: None,
1074 frame_vars: None,
1075 tool_telemetry: None,
1076 }],
1077 duration_ms: 10,
1078 frame_vars: None,
1079 fitness_delta: None,
1080 };
1081 let node = AinlMemoryNode {
1082 id: Uuid::nil(),
1083 memory_category: MemoryCategory::Trajectory,
1084 importance_score: 0.5,
1085 agent_id: "agent".into(),
1086 project_id: None,
1087 node_type: AinlNodeType::Trajectory { trajectory: traj },
1088 edges: Vec::new(),
1089 plugin_data: None,
1090 };
1091 let json = serde_json::to_string(&node).expect("serialize");
1092 let back: AinlMemoryNode = serde_json::from_str(&json).expect("deserialize");
1093 assert!(matches!(back.node_type, AinlNodeType::Trajectory { .. }));
1094 assert_eq!(back.trajectory().map(|t| t.episode_id), Some(Uuid::nil()));
1095 }
1096
1097 #[test]
1098 fn extension_from_typed_roundtrip() {
1099 #[derive(Serialize, Deserialize, PartialEq, Debug)]
1100 struct Demo {
1101 label: String,
1102 count: u32,
1103 }
1104 let demo = Demo {
1105 label: "probe".into(),
1106 count: 3,
1107 };
1108 let node_ty = AinlNodeType::extension_from_typed("demo.v1", 1, &demo).expect("typed ext");
1109 let back: Demo = node_ty.extension_downcast().expect("downcast");
1110 assert_eq!(back, demo);
1111 }
1112}