1use std::collections::{BTreeMap, HashMap, HashSet};
7
8use crate::graph::{Graph, GraphNodeOpts};
9use crate::json::{
10 non_negative_decimal_string_to_u128, strict_canonical_json_bytes, strict_json_decode,
11 u128_to_non_negative_decimal_string, validate_strict_json_value, Codec, JsonCodecError,
12 JsonCodecResult, JsonValue,
13};
14use crate::node::{Node, NodeOpts};
15use crate::operators::Operator;
16use crate::patterns::{
17 memory_retrieval_bundle, validate_memory_fragment, FactId, KnowledgeAssertion,
18 KnowledgeAssertionObject, MemoryAnswer, MemoryFragment, MemoryRetrievalBundle,
19 MemoryRetrievalBundleOptions, MemoryRetrievalError, MemoryRetrievalIndex, MemoryRetrievalQuery,
20 MemoryRetrievalSnapshot, MemoryRetrievalStatus, MemoryRetrievalStatusState,
21};
22use serde_json::{Map as JsonMap, Number as JsonNumber};
23
24pub const AGENTIC_MEMORY_RECORD_FRAME_FORMAT: &str = "graphrefly.agenticMemoryRecord";
26pub const AGENTIC_MEMORY_RECORD_FRAME_VERSION: u32 = 1;
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum AgenticMemoryKind {
32 Working,
34 Episodic,
36 Semantic,
38 Procedural,
40 Profile,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub enum AgenticMemoryPersistenceLevel {
47 Turn,
49 Session,
51 Project,
53 LongTerm,
55 Permanent,
57 Archived,
59}
60
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum AgenticMemoryArtifactKind {
64 Raw,
66 Insight,
68 Profile,
70 Procedure,
72}
73
74#[derive(Clone, Debug, Default, PartialEq, Eq)]
75pub struct AgenticMemoryScope {
77 pub session_id: Option<String>,
79 pub project_id: Option<String>,
81 pub user_id: Option<String>,
83 pub tenant_id: Option<String>,
85}
86
87#[derive(Clone, Debug, PartialEq)]
88pub struct AgenticMemoryRecord<T> {
90 pub id: FactId,
92 pub fragment: MemoryFragment<T>,
94 pub kind: AgenticMemoryKind,
96 pub persistence_level: AgenticMemoryPersistenceLevel,
98 pub artifact_kind: AgenticMemoryArtifactKind,
100 pub scope: Option<AgenticMemoryScope>,
102}
103
104#[derive(Clone, Debug, PartialEq, Eq)]
105pub struct AgenticMemoryFieldValidation {
107 pub ok: bool,
109 pub errors: Vec<String>,
111}
112
113#[derive(Clone, Debug, PartialEq, Eq)]
114pub struct AgenticMemoryRecordValidation {
116 pub ok: bool,
118 pub errors: Vec<String>,
120}
121
122#[derive(Clone, Debug, PartialEq, Eq)]
123pub struct AgenticMemoryRecordMetadata {
125 pub record_id: FactId,
127 pub kind: AgenticMemoryKind,
129 pub persistence_level: AgenticMemoryPersistenceLevel,
131 pub artifact_kind: AgenticMemoryArtifactKind,
133 pub scope: Option<AgenticMemoryScope>,
135}
136
137#[derive(Clone, Debug, PartialEq)]
138pub struct AgenticMemorySourceProjection {
140 pub fragment_id: FactId,
142 pub sources: Vec<FactId>,
144 pub parent_fragment_id: Option<FactId>,
146 pub provenance: Option<String>,
148 pub metadata: AgenticMemoryRecordMetadata,
150}
151
152#[derive(Clone, Copy, Debug, PartialEq, Eq)]
153pub enum AgenticMemoryStatusState {
155 Ready,
157 Empty,
159 Partial,
161 Error,
163}
164
165#[derive(Clone, Debug, PartialEq, Eq)]
166pub struct AgenticMemoryCursor {
168 pub evaluation: u64,
170 pub valid_records: usize,
172 pub invalid_records: usize,
174 pub projected_fragments: usize,
176 pub result_count: usize,
178}
179
180#[derive(Clone, Debug, PartialEq)]
181pub struct AgenticMemoryStatus {
183 pub state: AgenticMemoryStatusState,
185 pub query: MemoryRetrievalQuery,
187 pub cursor: AgenticMemoryCursor,
189 pub retrieval_status: MemoryRetrievalStatus,
191}
192
193#[derive(Clone, Copy, Debug, PartialEq, Eq)]
194pub enum AgenticMemoryErrorCode {
196 DuplicateRecordId,
198 DuplicateFragmentId,
200 InvalidRecord,
202 InvalidScope,
204 InvalidFragment,
206 InvalidKgDraft,
208 DuplicateAssertionId,
210 InvalidRetentionCommand,
212 DuplicateRetentionCommandId,
214 InvalidConsolidationOutcome,
216 DuplicateConsolidationOutcomeId,
218 MissingConsolidationRequest,
220 InvalidPackingPolicy,
222 InvalidTextProjection,
224 DuplicateTextProjection,
226 MissingTextProjection,
228}
229
230#[derive(Clone, Debug, PartialEq, Eq)]
231pub struct AgenticMemoryError {
233 pub code: AgenticMemoryErrorCode,
235 pub message: String,
237 pub index: Option<usize>,
239 pub record_id: Option<FactId>,
241 pub fragment_id: Option<FactId>,
243 pub validation_errors: Vec<String>,
245 pub cursor: AgenticMemoryCursor,
247}
248
249#[derive(Clone, Debug, PartialEq)]
250pub struct AgenticMemoryRecordFrame {
252 pub format: String,
254 pub version: u32,
256 pub record: AgenticMemoryRecord<JsonValue>,
258}
259
260#[derive(Clone, Copy, Debug, Default)]
261pub struct AgenticMemoryRecordFrameCodec;
263
264pub fn agentic_memory_record_frame(
266 record: AgenticMemoryRecord<JsonValue>,
267) -> AgenticMemoryRecordFrame {
268 AgenticMemoryRecordFrame {
269 format: AGENTIC_MEMORY_RECORD_FRAME_FORMAT.to_owned(),
270 version: AGENTIC_MEMORY_RECORD_FRAME_VERSION,
271 record,
272 }
273}
274
275pub fn agentic_memory_record_frame_codec() -> AgenticMemoryRecordFrameCodec {
277 AgenticMemoryRecordFrameCodec
278}
279
280#[derive(Clone, Debug, PartialEq)]
281pub struct AgenticMemoryKgAssertionDraft {
283 pub id: FactId,
285 pub record_id: Option<FactId>,
287 pub fragment_id: Option<FactId>,
289 pub subject_id: FactId,
291 pub predicate: String,
293 pub object: KnowledgeAssertionObject,
295 pub confidence: f64,
297 pub t_ns: u128,
299}
300
301#[derive(Clone, Debug, PartialEq, Eq)]
302pub struct AgenticMemoryKgProjectionCursor {
304 pub evaluation: u64,
306 pub valid_records: usize,
308 pub valid_drafts: usize,
310 pub invalid_drafts: usize,
312 pub projected_assertions: usize,
314}
315
316#[derive(Clone, Debug, PartialEq)]
317pub struct AgenticMemoryKgProjectionStatus {
319 pub state: AgenticMemoryStatusState,
321 pub cursor: AgenticMemoryKgProjectionCursor,
323}
324
325#[derive(Clone, Debug, PartialEq)]
326pub struct AgenticMemoryKgProjectionSnapshot {
328 pub assertions: Vec<KnowledgeAssertion>,
330 pub status: AgenticMemoryKgProjectionStatus,
332 pub errors: Vec<AgenticMemoryError>,
334 pub cursor: AgenticMemoryKgProjectionCursor,
336}
337
338#[derive(Clone)]
339pub struct AgenticMemoryKgProjectionBundleOptions<T> {
341 pub name: Option<String>,
343 pub records: Node<Vec<AgenticMemoryRecord<T>>>,
345 pub drafts: Node<Vec<AgenticMemoryKgAssertionDraft>>,
347}
348
349impl<T> AgenticMemoryKgProjectionBundleOptions<T> {
350 pub fn new(
352 records: Node<Vec<AgenticMemoryRecord<T>>>,
353 drafts: Node<Vec<AgenticMemoryKgAssertionDraft>>,
354 ) -> Self {
355 Self {
356 name: None,
357 records,
358 drafts,
359 }
360 }
361
362 pub fn named(mut self, name: impl Into<String>) -> Self {
364 self.name = Some(name.into());
365 self
366 }
367}
368
369#[derive(Clone)]
370pub struct AgenticMemoryKgProjectionBundle<T> {
372 pub records_input: Node<Vec<AgenticMemoryRecord<T>>>,
374 pub drafts_input: Node<Vec<AgenticMemoryKgAssertionDraft>>,
376 pub snapshot: Node<AgenticMemoryKgProjectionSnapshot>,
378 pub assertions: Node<Vec<KnowledgeAssertion>>,
380 pub status: Node<AgenticMemoryKgProjectionStatus>,
382 pub errors: Node<Vec<AgenticMemoryError>>,
384 pub cursor: Node<AgenticMemoryKgProjectionCursor>,
386}
387
388#[derive(Clone, Copy, Debug, PartialEq, Eq)]
389pub enum AgenticMemoryRetentionCommandKind {
391 Archive,
393 Restore,
395 RequestConsolidation,
397}
398
399#[derive(Clone, Debug, PartialEq, Eq)]
400pub struct AgenticMemoryRetentionCommand {
402 pub id: FactId,
404 pub record_id: FactId,
406 pub kind: AgenticMemoryRetentionCommandKind,
408 pub reason: Option<String>,
410}
411
412#[derive(Clone, Debug, PartialEq, Eq)]
413pub struct AgenticMemoryConsolidationRequest {
415 pub command_id: FactId,
417 pub record_id: FactId,
419 pub fragment_id: FactId,
421 pub reason: Option<String>,
423}
424
425#[derive(Clone, Debug, PartialEq, Eq)]
426pub struct AgenticMemoryRetentionCursor {
428 pub evaluation: u64,
430 pub valid_records: usize,
432 pub valid_commands: usize,
434 pub invalid_commands: usize,
436 pub active_records: usize,
438 pub archived_records: usize,
440 pub consolidation_requests: usize,
442}
443
444#[derive(Clone, Debug, PartialEq)]
445pub struct AgenticMemoryRetentionStatus {
447 pub state: AgenticMemoryStatusState,
449 pub cursor: AgenticMemoryRetentionCursor,
451}
452
453#[derive(Clone, Debug, PartialEq)]
454pub struct AgenticMemoryRetentionSnapshot<T> {
456 pub active_records: Vec<AgenticMemoryRecord<T>>,
458 pub archived_records: Vec<AgenticMemoryRecord<T>>,
460 pub consolidation_requests: Vec<AgenticMemoryConsolidationRequest>,
462 pub status: AgenticMemoryRetentionStatus,
464 pub errors: Vec<AgenticMemoryError>,
466 pub cursor: AgenticMemoryRetentionCursor,
468}
469
470#[derive(Clone)]
471pub struct AgenticMemoryRetentionBundleOptions<T> {
473 pub name: Option<String>,
475 pub records: Node<Vec<AgenticMemoryRecord<T>>>,
477 pub commands: Node<Vec<AgenticMemoryRetentionCommand>>,
479}
480
481impl<T> AgenticMemoryRetentionBundleOptions<T> {
482 pub fn new(
484 records: Node<Vec<AgenticMemoryRecord<T>>>,
485 commands: Node<Vec<AgenticMemoryRetentionCommand>>,
486 ) -> Self {
487 Self {
488 name: None,
489 records,
490 commands,
491 }
492 }
493
494 pub fn named(mut self, name: impl Into<String>) -> Self {
496 self.name = Some(name.into());
497 self
498 }
499}
500
501#[derive(Clone)]
502pub struct AgenticMemoryRetentionBundle<T> {
504 pub records_input: Node<Vec<AgenticMemoryRecord<T>>>,
506 pub commands_input: Node<Vec<AgenticMemoryRetentionCommand>>,
508 pub snapshot: Node<AgenticMemoryRetentionSnapshot<T>>,
510 pub active_records: Node<Vec<AgenticMemoryRecord<T>>>,
512 pub archived_records: Node<Vec<AgenticMemoryRecord<T>>>,
514 pub consolidation_requests: Node<Vec<AgenticMemoryConsolidationRequest>>,
516 pub status: Node<AgenticMemoryRetentionStatus>,
518 pub errors: Node<Vec<AgenticMemoryError>>,
520 pub cursor: Node<AgenticMemoryRetentionCursor>,
522}
523
524#[derive(Clone, Debug, PartialEq)]
525pub enum AgenticMemoryConsolidationOutcome<T> {
527 ProposedRecords {
529 id: FactId,
531 request_id: FactId,
533 records: Vec<AgenticMemoryRecord<T>>,
535 provenance: Option<String>,
537 },
538 Failed {
540 id: FactId,
542 request_id: FactId,
544 message: String,
546 provenance: Option<String>,
548 },
549}
550
551#[derive(Clone, Debug, PartialEq)]
552pub struct AgenticMemoryConsolidationRecordDraft<T> {
554 pub id: FactId,
556 pub request_id: FactId,
558 pub outcome_id: FactId,
560 pub record: AgenticMemoryRecord<T>,
562}
563
564#[derive(Clone, Debug, PartialEq, Eq)]
565pub enum AgenticMemoryConsolidationCommandKind {
567 ProposeRecords,
569 MarkFailed,
571}
572
573#[derive(Clone, Debug, PartialEq, Eq)]
574pub struct AgenticMemoryConsolidationCommand {
576 pub id: FactId,
578 pub kind: AgenticMemoryConsolidationCommandKind,
580 pub request_id: FactId,
582 pub outcome_id: FactId,
584 pub draft_ids: Vec<FactId>,
586 pub message: Option<String>,
588}
589
590#[derive(Clone, Debug, PartialEq, Eq)]
591pub enum AgenticMemoryConsolidationResultState {
593 Proposed,
595 Failed,
597}
598
599#[derive(Clone, Debug, PartialEq, Eq)]
600pub struct AgenticMemoryConsolidationResult {
602 pub id: FactId,
604 pub request_id: FactId,
606 pub outcome_id: FactId,
608 pub state: AgenticMemoryConsolidationResultState,
610 pub source_record_ids: Vec<FactId>,
612 pub proposed_record_ids: Vec<FactId>,
614 pub message: Option<String>,
616 pub provenance: Option<String>,
618}
619
620#[derive(Clone, Debug, PartialEq, Eq)]
621pub struct AgenticMemoryConsolidationCursor {
623 pub evaluation: u64,
625 pub valid_requests: usize,
627 pub valid_outcomes: usize,
629 pub invalid_outcomes: usize,
631 pub results: usize,
633 pub proposed_record_drafts: usize,
635}
636
637#[derive(Clone, Debug, PartialEq)]
638pub struct AgenticMemoryConsolidationStatus {
640 pub state: AgenticMemoryStatusState,
642 pub cursor: AgenticMemoryConsolidationCursor,
644}
645
646#[derive(Clone, Debug, PartialEq)]
647pub struct AgenticMemoryConsolidationSnapshot<T> {
649 pub results: Vec<AgenticMemoryConsolidationResult>,
651 pub proposed_record_drafts: Vec<AgenticMemoryConsolidationRecordDraft<T>>,
653 pub commands: Vec<AgenticMemoryConsolidationCommand>,
655 pub status: AgenticMemoryConsolidationStatus,
657 pub errors: Vec<AgenticMemoryError>,
659 pub cursor: AgenticMemoryConsolidationCursor,
661}
662
663#[derive(Clone)]
664pub struct AgenticMemoryConsolidationBundleOptions<T> {
666 pub name: Option<String>,
668 pub requests: Node<Vec<AgenticMemoryConsolidationRequest>>,
670 pub outcomes: Node<Vec<AgenticMemoryConsolidationOutcome<T>>>,
672}
673
674impl<T> AgenticMemoryConsolidationBundleOptions<T> {
675 pub fn new(
677 requests: Node<Vec<AgenticMemoryConsolidationRequest>>,
678 outcomes: Node<Vec<AgenticMemoryConsolidationOutcome<T>>>,
679 ) -> Self {
680 Self {
681 name: None,
682 requests,
683 outcomes,
684 }
685 }
686
687 pub fn named(mut self, name: impl Into<String>) -> Self {
689 self.name = Some(name.into());
690 self
691 }
692}
693
694#[derive(Clone)]
695pub struct AgenticMemoryConsolidationBundle<T> {
697 pub requests_input: Node<Vec<AgenticMemoryConsolidationRequest>>,
699 pub outcomes_input: Node<Vec<AgenticMemoryConsolidationOutcome<T>>>,
701 pub snapshot: Node<AgenticMemoryConsolidationSnapshot<T>>,
703 pub results: Node<Vec<AgenticMemoryConsolidationResult>>,
705 pub proposed_record_drafts: Node<Vec<AgenticMemoryConsolidationRecordDraft<T>>>,
707 pub commands: Node<Vec<AgenticMemoryConsolidationCommand>>,
709 pub status: Node<AgenticMemoryConsolidationStatus>,
711 pub errors: Node<Vec<AgenticMemoryError>>,
713 pub cursor: Node<AgenticMemoryConsolidationCursor>,
715}
716
717#[derive(Clone, Debug, PartialEq, Eq)]
718pub struct AgenticMemoryTextProjection {
720 pub fragment_id: FactId,
722 pub text: String,
724}
725
726#[derive(Clone, Debug, PartialEq, Eq)]
727pub struct AgenticMemoryContextPackingPolicy {
729 pub max_chars: Option<usize>,
731 pub separator: String,
733 pub include_fragment_ids: bool,
735}
736
737impl Default for AgenticMemoryContextPackingPolicy {
738 fn default() -> Self {
739 Self {
740 max_chars: None,
741 separator: "\n\n".to_owned(),
742 include_fragment_ids: false,
743 }
744 }
745}
746
747#[derive(Clone, Debug, PartialEq, Eq)]
748pub struct AgenticMemoryContextPackingCursor {
750 pub evaluation: u64,
752 pub context_entries: usize,
754 pub text_projection_count: usize,
756 pub packed_entries: usize,
758 pub missing_text: usize,
760 pub char_count: usize,
762}
763
764#[derive(Clone, Debug, PartialEq, Eq)]
765pub struct AgenticMemoryPackedContext {
767 pub text: String,
769 pub fragment_ids: Vec<FactId>,
771 pub truncated: bool,
773 pub cursor: AgenticMemoryContextPackingCursor,
775}
776
777#[derive(Clone, Debug, PartialEq)]
778pub struct AgenticMemoryContextPackingStatus {
780 pub state: AgenticMemoryStatusState,
782 pub cursor: AgenticMemoryContextPackingCursor,
784}
785
786#[derive(Clone, Debug, PartialEq)]
787pub struct AgenticMemoryContextPackingSnapshot {
789 pub packed_context: AgenticMemoryPackedContext,
791 pub status: AgenticMemoryContextPackingStatus,
793 pub errors: Vec<AgenticMemoryError>,
795 pub cursor: AgenticMemoryContextPackingCursor,
797}
798
799#[derive(Clone)]
800pub struct AgenticMemoryContextPackingBundleOptions<T> {
802 pub name: Option<String>,
804 pub context: Node<AgenticMemoryContext<T>>,
806 pub texts: Node<Vec<AgenticMemoryTextProjection>>,
808 pub policy: Node<AgenticMemoryContextPackingPolicy>,
810}
811
812impl<T> AgenticMemoryContextPackingBundleOptions<T> {
813 pub fn new(
815 context: Node<AgenticMemoryContext<T>>,
816 texts: Node<Vec<AgenticMemoryTextProjection>>,
817 policy: Node<AgenticMemoryContextPackingPolicy>,
818 ) -> Self {
819 Self {
820 name: None,
821 context,
822 texts,
823 policy,
824 }
825 }
826
827 pub fn named(mut self, name: impl Into<String>) -> Self {
829 self.name = Some(name.into());
830 self
831 }
832}
833
834#[derive(Clone)]
835pub struct AgenticMemoryContextPackingBundle<T> {
837 pub context_input: Node<AgenticMemoryContext<T>>,
839 pub texts_input: Node<Vec<AgenticMemoryTextProjection>>,
841 pub policy_input: Node<AgenticMemoryContextPackingPolicy>,
843 pub snapshot: Node<AgenticMemoryContextPackingSnapshot>,
845 pub packed_context: Node<AgenticMemoryPackedContext>,
847 pub status: Node<AgenticMemoryContextPackingStatus>,
849 pub errors: Node<Vec<AgenticMemoryError>>,
851 pub cursor: Node<AgenticMemoryContextPackingCursor>,
853}
854
855#[derive(Clone, Debug, PartialEq)]
856pub struct AgenticMemoryContextEntry<T> {
858 pub fragment_id: FactId,
860 pub payload: T,
862 pub confidence: f64,
864 pub tags: Vec<String>,
866 pub sources: Vec<FactId>,
868 pub fragment: MemoryFragment<T>,
870 pub metadata: Option<AgenticMemoryRecordMetadata>,
872}
873
874#[derive(Clone, Debug, PartialEq)]
875pub struct AgenticMemoryContext<T> {
877 pub state: AgenticMemoryStatusState,
879 pub query: MemoryRetrievalQuery,
881 pub entries: Vec<AgenticMemoryContextEntry<T>>,
883 pub cursor: AgenticMemoryCursor,
885 pub errors: Vec<AgenticMemoryError>,
887 pub retrieval_status: MemoryRetrievalStatus,
889 pub retrieval_errors: Vec<MemoryRetrievalError>,
891 pub context_ready: bool,
893}
894
895#[derive(Clone, Debug, PartialEq)]
896pub struct AgenticMemoryProjection<T> {
898 pub records: Vec<AgenticMemoryRecord<T>>,
900 pub fragments: Vec<MemoryFragment<T>>,
902 pub metadata_by_fragment_id: BTreeMap<FactId, AgenticMemoryRecordMetadata>,
904 pub sources: Vec<AgenticMemorySourceProjection>,
906 pub errors: Vec<AgenticMemoryError>,
908 pub cursor: AgenticMemoryCursor,
910}
911
912#[derive(Clone)]
913pub struct AgenticMemoryBundleOptions<T> {
915 pub name: Option<String>,
917 pub records: Node<Vec<AgenticMemoryRecord<T>>>,
919 pub query: Node<MemoryRetrievalQuery>,
921}
922
923impl<T> AgenticMemoryBundleOptions<T> {
924 pub fn new(
926 records: Node<Vec<AgenticMemoryRecord<T>>>,
927 query: Node<MemoryRetrievalQuery>,
928 ) -> Self {
929 Self {
930 name: None,
931 records,
932 query,
933 }
934 }
935
936 pub fn named(mut self, name: impl Into<String>) -> Self {
938 self.name = Some(name.into());
939 self
940 }
941}
942
943#[derive(Clone)]
944pub struct AgenticMemoryBundle<T> {
946 pub records_input: Node<Vec<AgenticMemoryRecord<T>>>,
948 pub query_input: Node<MemoryRetrievalQuery>,
950 pub projection: Node<AgenticMemoryProjection<T>>,
952 pub retrieval: MemoryRetrievalBundle<T>,
954 pub retrieval_snapshot: Node<MemoryRetrievalSnapshot<T>>,
956 pub retrieval_status: Node<MemoryRetrievalStatus>,
958 pub retrieval_errors: Node<Vec<MemoryRetrievalError>>,
960 pub fragments: Node<Vec<MemoryFragment<T>>>,
962 pub sources: Node<Vec<AgenticMemorySourceProjection>>,
964 pub indexed: Node<MemoryRetrievalIndex<T>>,
966 pub ranked: Node<MemoryAnswer<T>>,
968 pub context: Node<AgenticMemoryContext<T>>,
970 pub status: Node<AgenticMemoryStatus>,
972 pub errors: Node<Vec<AgenticMemoryError>>,
974 pub cursor: Node<AgenticMemoryCursor>,
976}
977
978pub fn validate_agentic_memory_kind(_kind: &AgenticMemoryKind) -> AgenticMemoryFieldValidation {
980 AgenticMemoryFieldValidation {
981 ok: true,
982 errors: Vec::new(),
983 }
984}
985
986pub fn validate_agentic_memory_persistence_level(
988 _level: &AgenticMemoryPersistenceLevel,
989) -> AgenticMemoryFieldValidation {
990 AgenticMemoryFieldValidation {
991 ok: true,
992 errors: Vec::new(),
993 }
994}
995
996pub fn validate_agentic_memory_artifact_kind(
998 _kind: &AgenticMemoryArtifactKind,
999) -> AgenticMemoryFieldValidation {
1000 AgenticMemoryFieldValidation {
1001 ok: true,
1002 errors: Vec::new(),
1003 }
1004}
1005
1006pub fn validate_agentic_memory_scope(scope: &AgenticMemoryScope) -> AgenticMemoryFieldValidation {
1008 let mut errors = Vec::new();
1009 if scope
1010 .session_id
1011 .as_ref()
1012 .is_some_and(|value| value.is_empty())
1013 {
1014 errors.push("scope.session_id must be a non-empty string when present".to_owned());
1015 }
1016 if scope
1017 .project_id
1018 .as_ref()
1019 .is_some_and(|value| value.is_empty())
1020 {
1021 errors.push("scope.project_id must be a non-empty string when present".to_owned());
1022 }
1023 if scope.user_id.as_ref().is_some_and(|value| value.is_empty()) {
1024 errors.push("scope.user_id must be a non-empty string when present".to_owned());
1025 }
1026 if scope
1027 .tenant_id
1028 .as_ref()
1029 .is_some_and(|value| value.is_empty())
1030 {
1031 errors.push("scope.tenant_id must be a non-empty string when present".to_owned());
1032 }
1033 AgenticMemoryFieldValidation {
1034 ok: errors.is_empty(),
1035 errors,
1036 }
1037}
1038
1039pub fn validate_agentic_memory_record<T>(
1041 record: &AgenticMemoryRecord<T>,
1042) -> AgenticMemoryRecordValidation {
1043 let mut errors = Vec::new();
1044 if record.id.is_empty() {
1045 errors.push("id must be a non-empty string".to_owned());
1046 }
1047 let fragment_validation = validate_memory_fragment(&record.fragment);
1048 errors.extend(
1049 fragment_validation
1050 .errors
1051 .into_iter()
1052 .map(|error| format!("fragment.{error}")),
1053 );
1054 if let Some(scope) = &record.scope {
1055 errors.extend(validate_agentic_memory_scope(scope).errors);
1056 }
1057 AgenticMemoryRecordValidation {
1058 ok: errors.is_empty(),
1059 errors,
1060 }
1061}
1062
1063pub fn agentic_memory_bundle<T: Clone + 'static>(
1070 graph: &Graph,
1071 opts: AgenticMemoryBundleOptions<T>,
1072) -> AgenticMemoryBundle<T> {
1073 let name = opts.name.unwrap_or_else(|| "agenticMemory".to_owned());
1074 let records = opts.records;
1075 let query = opts.query;
1076 let projection = graph.init_node(
1077 Operator::with_opts("agenticMemoryProjection", solution_node_config(), |ctx| {
1078 let evaluation = ctx
1079 .state_get::<u64>()
1080 .map(|evaluation| *evaluation + 1)
1081 .unwrap_or(1);
1082 let raw_records = ctx
1083 .data::<Vec<AgenticMemoryRecord<T>>>(0)
1084 .map(|records| (*records).clone())
1085 .unwrap_or_default();
1086 let mut records = Vec::new();
1087 let mut fragments = Vec::new();
1088 let mut metadata_by_fragment_id = BTreeMap::new();
1089 let mut sources = Vec::new();
1090 let mut pending_errors = Vec::<PendingAgenticMemoryError>::new();
1091 let mut seen_record_ids = HashSet::<FactId>::new();
1092 let mut seen_fragment_ids = HashSet::<FactId>::new();
1093
1094 for (index, record) in raw_records.into_iter().enumerate() {
1095 let validation = validate_agentic_memory_record(&record);
1096 if !validation.ok {
1097 pending_errors.push(PendingAgenticMemoryError {
1098 code: AgenticMemoryErrorCode::InvalidRecord,
1099 index: Some(index),
1100 record_id: Some(record.id.clone()),
1101 fragment_id: Some(record.fragment.id.clone()),
1102 validation_errors: validation.errors,
1103 });
1104 continue;
1105 }
1106 if !seen_record_ids.insert(record.id.clone()) {
1107 pending_errors.push(PendingAgenticMemoryError {
1108 code: AgenticMemoryErrorCode::DuplicateRecordId,
1109 index: Some(index),
1110 record_id: Some(record.id.clone()),
1111 fragment_id: Some(record.fragment.id.clone()),
1112 validation_errors: vec![format!("duplicate record id '{}'", record.id)],
1113 });
1114 continue;
1115 }
1116 if !seen_fragment_ids.insert(record.fragment.id.clone()) {
1117 pending_errors.push(PendingAgenticMemoryError {
1118 code: AgenticMemoryErrorCode::DuplicateFragmentId,
1119 index: Some(index),
1120 record_id: Some(record.id.clone()),
1121 fragment_id: Some(record.fragment.id.clone()),
1122 validation_errors: vec![format!(
1123 "duplicate fragment id '{}'",
1124 record.fragment.id
1125 )],
1126 });
1127 continue;
1128 }
1129 let metadata = record_metadata(&record);
1130 metadata_by_fragment_id
1131 .entry(record.fragment.id.clone())
1132 .or_insert_with(|| metadata.clone());
1133 sources.push(AgenticMemorySourceProjection {
1134 fragment_id: record.fragment.id.clone(),
1135 sources: record.fragment.sources.clone(),
1136 parent_fragment_id: record.fragment.parent_fragment_id.clone(),
1137 provenance: record.fragment.provenance.clone(),
1138 metadata,
1139 });
1140 fragments.push(record.fragment.clone());
1141 records.push(record);
1142 }
1143
1144 let cursor = AgenticMemoryCursor {
1145 evaluation,
1146 valid_records: records.len(),
1147 invalid_records: pending_errors.len(),
1148 projected_fragments: fragments.len(),
1149 result_count: 0,
1150 };
1151 ctx.state_set(evaluation);
1152 let errors = pending_errors
1153 .into_iter()
1154 .map(|error| AgenticMemoryError {
1155 code: error.code,
1156 message: "agentic_memory_bundle: invalid agentic memory record".to_owned(),
1157 index: error.index,
1158 record_id: error.record_id,
1159 fragment_id: error.fragment_id,
1160 validation_errors: error.validation_errors,
1161 cursor: cursor.clone(),
1162 })
1163 .collect();
1164 ctx.emit(AgenticMemoryProjection {
1165 records,
1166 fragments,
1167 metadata_by_fragment_id,
1168 sources,
1169 errors,
1170 cursor,
1171 });
1172 }),
1173 vec![records.erased()],
1174 named_solution_node_opts(format!("{name}/projection")),
1175 );
1176 let fragments = agentic_projection(
1177 graph,
1178 &projection,
1179 format!("{name}/fragments"),
1180 "agenticMemoryFragments",
1181 |projection| projection.fragments.clone(),
1182 );
1183 let retrieval = memory_retrieval_bundle(
1184 graph,
1185 MemoryRetrievalBundleOptions::new(fragments.clone(), query.clone())
1186 .named(format!("{name}/retrieval")),
1187 );
1188 let context = graph.init_node(
1189 Operator::with_opts("agenticMemoryContext", solution_node_config(), |ctx| {
1190 let Some(projection) = ctx.data::<AgenticMemoryProjection<T>>(0) else {
1191 return;
1192 };
1193 let Some(snapshot) = ctx.data::<MemoryRetrievalSnapshot<T>>(1) else {
1194 return;
1195 };
1196 ctx.emit(context_from_snapshot(
1197 projection.as_ref(),
1198 snapshot.as_ref(),
1199 ));
1200 }),
1201 vec![projection.erased(), retrieval.snapshot.erased()],
1202 named_solution_node_opts(format!("{name}/context")),
1203 );
1204
1205 AgenticMemoryBundle {
1206 records_input: records,
1207 query_input: query,
1208 sources: agentic_projection(
1209 graph,
1210 &projection,
1211 format!("{name}/sources"),
1212 "agenticMemorySources",
1213 |projection| projection.sources.clone(),
1214 ),
1215 status: context_projection(
1216 graph,
1217 &context,
1218 format!("{name}/status"),
1219 "agenticMemoryStatus",
1220 |context| AgenticMemoryStatus {
1221 state: context.state,
1222 query: context.query.clone(),
1223 cursor: context.cursor.clone(),
1224 retrieval_status: context.retrieval_status.clone(),
1225 },
1226 ),
1227 errors: context_projection(
1228 graph,
1229 &context,
1230 format!("{name}/errors"),
1231 "agenticMemoryErrors",
1232 |context| context.errors.clone(),
1233 ),
1234 cursor: context_projection(
1235 graph,
1236 &context,
1237 format!("{name}/cursor"),
1238 "agenticMemoryCursor",
1239 |context| context.cursor.clone(),
1240 ),
1241 indexed: retrieval.indexed.clone(),
1242 ranked: retrieval.ranked.clone(),
1243 retrieval_snapshot: retrieval.snapshot.clone(),
1244 retrieval_status: retrieval.status.clone(),
1245 retrieval_errors: retrieval.errors.clone(),
1246 projection,
1247 fragments,
1248 context,
1249 retrieval,
1250 }
1251}
1252
1253pub fn agentic_memory_kg_projection_bundle<T: Clone + 'static>(
1255 graph: &Graph,
1256 opts: AgenticMemoryKgProjectionBundleOptions<T>,
1257) -> AgenticMemoryKgProjectionBundle<T> {
1258 let name = opts
1259 .name
1260 .unwrap_or_else(|| "agenticMemoryKgProjection".to_owned());
1261 let records = opts.records;
1262 let drafts = opts.drafts;
1263 let snapshot = graph.init_node(
1264 Operator::with_opts("agenticMemoryKgProjection", solution_node_config(), |ctx| {
1265 let evaluation = ctx
1266 .state_get::<u64>()
1267 .map(|evaluation| *evaluation + 1)
1268 .unwrap_or(1);
1269 let raw_records = ctx
1270 .data::<Vec<AgenticMemoryRecord<T>>>(0)
1271 .map(|records| (*records).clone())
1272 .unwrap_or_default();
1273 let raw_drafts = ctx
1274 .data::<Vec<AgenticMemoryKgAssertionDraft>>(1)
1275 .map(|drafts| (*drafts).clone())
1276 .unwrap_or_default();
1277 let valid_records = valid_record_index(raw_records);
1278 let invalid_record_errors = valid_records.errors.len();
1279 let fragment_ids = valid_records
1280 .by_record_id
1281 .values()
1282 .map(|record| record.fragment.id.clone())
1283 .collect::<HashSet<_>>();
1284 let mut seen_assertion_ids = HashSet::<FactId>::new();
1285 let mut assertions = Vec::new();
1286 let mut pending_errors = valid_records.errors;
1287
1288 for (index, draft) in raw_drafts.into_iter().enumerate() {
1289 let mut validation_errors = validate_kg_draft(&draft);
1290 if seen_assertion_ids.contains(&draft.id) {
1291 validation_errors.push(format!("duplicate assertion id '{}'", draft.id));
1292 }
1293 if let Some(record_id) = &draft.record_id {
1294 if !valid_records.by_record_id.contains_key(record_id) {
1295 validation_errors.push(format!(
1296 "record_id '{record_id}' does not reference a valid record"
1297 ));
1298 }
1299 }
1300 if let Some(fragment_id) = &draft.fragment_id {
1301 if !fragment_ids.contains(fragment_id) {
1302 validation_errors.push(format!(
1303 "fragment_id '{fragment_id}' does not reference a valid fragment"
1304 ));
1305 }
1306 }
1307 if let (Some(record_id), Some(fragment_id)) =
1308 (&draft.record_id, &draft.fragment_id)
1309 {
1310 if let Some(record) = valid_records.by_record_id.get(record_id) {
1311 if record.fragment.id != *fragment_id {
1312 validation_errors.push(format!(
1313 "fragment_id '{fragment_id}' is not owned by record_id '{record_id}'"
1314 ));
1315 }
1316 }
1317 }
1318 if draft.record_id.is_none() && draft.fragment_id.is_none() {
1319 validation_errors
1320 .push("draft must reference record_id or fragment_id".to_owned());
1321 }
1322 if !validation_errors.is_empty() {
1323 pending_errors.push(PendingAgenticMemoryError {
1324 code: if validation_errors
1325 .iter()
1326 .any(|error| error.starts_with("duplicate assertion id"))
1327 {
1328 AgenticMemoryErrorCode::DuplicateAssertionId
1329 } else {
1330 AgenticMemoryErrorCode::InvalidKgDraft
1331 },
1332 index: Some(index),
1333 record_id: draft.record_id.clone(),
1334 fragment_id: draft.fragment_id.clone(),
1335 validation_errors,
1336 });
1337 continue;
1338 }
1339 seen_assertion_ids.insert(draft.id.clone());
1340 let mut sources = Vec::new();
1341 if let Some(record_id) = &draft.record_id {
1342 if let Some(record) = valid_records.by_record_id.get(record_id) {
1343 push_unique(&mut sources, record.fragment.id.clone());
1344 }
1345 }
1346 if let Some(fragment_id) = &draft.fragment_id {
1347 push_unique(&mut sources, fragment_id.clone());
1348 }
1349 assertions.push(KnowledgeAssertion {
1350 id: draft.id,
1351 subject_id: draft.subject_id,
1352 predicate: draft.predicate,
1353 object: draft.object,
1354 sources,
1355 confidence: draft.confidence,
1356 t_ns: draft.t_ns,
1357 });
1358 }
1359 let cursor = AgenticMemoryKgProjectionCursor {
1360 evaluation,
1361 valid_records: valid_records.by_record_id.len(),
1362 valid_drafts: assertions.len(),
1363 invalid_drafts: pending_errors.len().saturating_sub(invalid_record_errors),
1364 projected_assertions: assertions.len(),
1365 };
1366 let status = AgenticMemoryKgProjectionStatus {
1367 state: projection_state(
1368 pending_errors.len(),
1369 assertions.len(),
1370 valid_records.by_record_id.len(),
1371 ),
1372 cursor: cursor.clone(),
1373 };
1374 ctx.state_set(evaluation);
1375 let errors = pending_errors
1376 .into_iter()
1377 .map(|error| AgenticMemoryError {
1378 code: error.code,
1379 message: "agentic_memory_kg_projection_bundle: invalid KG assertion draft"
1380 .to_owned(),
1381 index: error.index,
1382 record_id: error.record_id,
1383 fragment_id: error.fragment_id,
1384 validation_errors: error.validation_errors,
1385 cursor: kg_error_cursor(&cursor),
1386 })
1387 .collect();
1388 ctx.emit(AgenticMemoryKgProjectionSnapshot {
1389 assertions,
1390 status,
1391 errors,
1392 cursor,
1393 });
1394 }),
1395 vec![records.erased(), drafts.erased()],
1396 named_solution_node_opts(format!("{name}/snapshot")),
1397 );
1398
1399 AgenticMemoryKgProjectionBundle {
1400 records_input: records,
1401 drafts_input: drafts,
1402 assertions: solution_projection(
1403 graph,
1404 &snapshot,
1405 format!("{name}/assertions"),
1406 "agenticMemoryKgAssertions",
1407 |snapshot: &AgenticMemoryKgProjectionSnapshot| snapshot.assertions.clone(),
1408 ),
1409 status: solution_projection(
1410 graph,
1411 &snapshot,
1412 format!("{name}/status"),
1413 "agenticMemoryKgStatus",
1414 |snapshot: &AgenticMemoryKgProjectionSnapshot| snapshot.status.clone(),
1415 ),
1416 errors: solution_projection(
1417 graph,
1418 &snapshot,
1419 format!("{name}/errors"),
1420 "agenticMemoryKgErrors",
1421 |snapshot: &AgenticMemoryKgProjectionSnapshot| snapshot.errors.clone(),
1422 ),
1423 cursor: solution_projection(
1424 graph,
1425 &snapshot,
1426 format!("{name}/cursor"),
1427 "agenticMemoryKgCursor",
1428 |snapshot: &AgenticMemoryKgProjectionSnapshot| snapshot.cursor.clone(),
1429 ),
1430 snapshot,
1431 }
1432}
1433
1434pub fn agentic_memory_retention_bundle<T: Clone + 'static>(
1436 graph: &Graph,
1437 opts: AgenticMemoryRetentionBundleOptions<T>,
1438) -> AgenticMemoryRetentionBundle<T> {
1439 let name = opts
1440 .name
1441 .unwrap_or_else(|| "agenticMemoryRetention".to_owned());
1442 let records = opts.records;
1443 let commands = opts.commands;
1444 let snapshot = graph.init_node(
1445 Operator::with_opts("agenticMemoryRetention", solution_node_config(), |ctx| {
1446 let evaluation = ctx
1447 .state_get::<u64>()
1448 .map(|evaluation| *evaluation + 1)
1449 .unwrap_or(1);
1450 let raw_records = ctx
1451 .data::<Vec<AgenticMemoryRecord<T>>>(0)
1452 .map(|records| (*records).clone())
1453 .unwrap_or_default();
1454 let raw_commands = ctx
1455 .data::<Vec<AgenticMemoryRetentionCommand>>(1)
1456 .map(|commands| (*commands).clone())
1457 .unwrap_or_default();
1458 let valid_records = valid_record_index(raw_records);
1459 let invalid_record_errors = valid_records.errors.len();
1460 let mut pending_errors = valid_records.errors;
1461 let mut archived = HashSet::<FactId>::new();
1462 let mut consolidation_requests = Vec::new();
1463 let mut seen_command_ids = HashSet::<FactId>::new();
1464 let mut valid_commands = 0usize;
1465
1466 for (index, command) in raw_commands.into_iter().enumerate() {
1467 let mut validation_errors = validate_retention_command(&command);
1468 if seen_command_ids.contains(&command.id) {
1469 validation_errors
1470 .push(format!("duplicate retention command id '{}'", command.id));
1471 }
1472 if !valid_records.by_record_id.contains_key(&command.record_id) {
1473 validation_errors.push(format!(
1474 "record_id '{}' does not reference a valid record",
1475 command.record_id
1476 ));
1477 }
1478 if !validation_errors.is_empty() {
1479 pending_errors.push(PendingAgenticMemoryError {
1480 code: if validation_errors
1481 .iter()
1482 .any(|error| error.starts_with("duplicate retention command id"))
1483 {
1484 AgenticMemoryErrorCode::DuplicateRetentionCommandId
1485 } else {
1486 AgenticMemoryErrorCode::InvalidRetentionCommand
1487 },
1488 index: Some(index),
1489 record_id: Some(command.record_id),
1490 fragment_id: None,
1491 validation_errors,
1492 });
1493 continue;
1494 }
1495 seen_command_ids.insert(command.id.clone());
1496 valid_commands += 1;
1497 match command.kind {
1498 AgenticMemoryRetentionCommandKind::Archive => {
1499 archived.insert(command.record_id);
1500 }
1501 AgenticMemoryRetentionCommandKind::Restore => {
1502 archived.remove(&command.record_id);
1503 }
1504 AgenticMemoryRetentionCommandKind::RequestConsolidation => {
1505 let record = valid_records
1506 .by_record_id
1507 .get(&command.record_id)
1508 .expect("command validation checked record existence");
1509 consolidation_requests.push(AgenticMemoryConsolidationRequest {
1510 command_id: command.id,
1511 record_id: command.record_id,
1512 fragment_id: record.fragment.id.clone(),
1513 reason: command.reason,
1514 });
1515 }
1516 }
1517 }
1518
1519 let mut active_records = Vec::new();
1520 let mut archived_records = Vec::new();
1521 for record in valid_records.in_order {
1522 if archived.contains(&record.id) {
1523 archived_records.push(record);
1524 } else {
1525 active_records.push(record);
1526 }
1527 }
1528 let cursor = AgenticMemoryRetentionCursor {
1529 evaluation,
1530 valid_records: active_records.len() + archived_records.len(),
1531 valid_commands,
1532 invalid_commands: pending_errors.len().saturating_sub(invalid_record_errors),
1533 active_records: active_records.len(),
1534 archived_records: archived_records.len(),
1535 consolidation_requests: consolidation_requests.len(),
1536 };
1537 let status = AgenticMemoryRetentionStatus {
1538 state: projection_state(
1539 pending_errors.len(),
1540 active_records.len() + archived_records.len(),
1541 active_records.len() + archived_records.len(),
1542 ),
1543 cursor: cursor.clone(),
1544 };
1545 ctx.state_set(evaluation);
1546 let errors = pending_errors
1547 .into_iter()
1548 .map(|error| AgenticMemoryError {
1549 code: error.code,
1550 message: "agentic_memory_retention_bundle: invalid retention input".to_owned(),
1551 index: error.index,
1552 record_id: error.record_id,
1553 fragment_id: error.fragment_id,
1554 validation_errors: error.validation_errors,
1555 cursor: retention_error_cursor(&cursor),
1556 })
1557 .collect();
1558 ctx.emit(AgenticMemoryRetentionSnapshot {
1559 active_records,
1560 archived_records,
1561 consolidation_requests,
1562 status,
1563 errors,
1564 cursor,
1565 });
1566 }),
1567 vec![records.erased(), commands.erased()],
1568 named_solution_node_opts(format!("{name}/snapshot")),
1569 );
1570
1571 AgenticMemoryRetentionBundle {
1572 records_input: records,
1573 commands_input: commands,
1574 active_records: solution_projection(
1575 graph,
1576 &snapshot,
1577 format!("{name}/active_records"),
1578 "agenticMemoryActiveRecords",
1579 |snapshot: &AgenticMemoryRetentionSnapshot<T>| snapshot.active_records.clone(),
1580 ),
1581 archived_records: solution_projection(
1582 graph,
1583 &snapshot,
1584 format!("{name}/archived_records"),
1585 "agenticMemoryArchivedRecords",
1586 |snapshot: &AgenticMemoryRetentionSnapshot<T>| snapshot.archived_records.clone(),
1587 ),
1588 consolidation_requests: solution_projection(
1589 graph,
1590 &snapshot,
1591 format!("{name}/consolidation_requests"),
1592 "agenticMemoryConsolidationRequests",
1593 |snapshot: &AgenticMemoryRetentionSnapshot<T>| snapshot.consolidation_requests.clone(),
1594 ),
1595 status: solution_projection(
1596 graph,
1597 &snapshot,
1598 format!("{name}/status"),
1599 "agenticMemoryRetentionStatus",
1600 |snapshot: &AgenticMemoryRetentionSnapshot<T>| snapshot.status.clone(),
1601 ),
1602 errors: solution_projection(
1603 graph,
1604 &snapshot,
1605 format!("{name}/errors"),
1606 "agenticMemoryRetentionErrors",
1607 |snapshot: &AgenticMemoryRetentionSnapshot<T>| snapshot.errors.clone(),
1608 ),
1609 cursor: solution_projection(
1610 graph,
1611 &snapshot,
1612 format!("{name}/cursor"),
1613 "agenticMemoryRetentionCursor",
1614 |snapshot: &AgenticMemoryRetentionSnapshot<T>| snapshot.cursor.clone(),
1615 ),
1616 snapshot,
1617 }
1618}
1619
1620pub fn agentic_memory_consolidation_bundle<T: Clone + 'static>(
1622 graph: &Graph,
1623 opts: AgenticMemoryConsolidationBundleOptions<T>,
1624) -> AgenticMemoryConsolidationBundle<T> {
1625 let name = opts
1626 .name
1627 .unwrap_or_else(|| "agenticMemoryConsolidation".to_owned());
1628 let requests = opts.requests;
1629 let outcomes = opts.outcomes;
1630 let snapshot = graph.init_node(
1631 Operator::with_opts(
1632 "agenticMemoryConsolidation",
1633 solution_node_config(),
1634 |ctx| {
1635 let evaluation = ctx
1636 .state_get::<u64>()
1637 .map(|evaluation| *evaluation + 1)
1638 .unwrap_or(1);
1639 let requests = ctx
1640 .data::<Vec<AgenticMemoryConsolidationRequest>>(0)
1641 .map(|requests| (*requests).clone())
1642 .unwrap_or_default();
1643 let outcomes = ctx
1644 .data::<Vec<AgenticMemoryConsolidationOutcome<T>>>(1)
1645 .map(|outcomes| (*outcomes).clone())
1646 .unwrap_or_default();
1647 let projected = project_consolidation_outcomes(requests, outcomes);
1648 let cursor = AgenticMemoryConsolidationCursor {
1649 evaluation,
1650 valid_requests: projected.valid_requests,
1651 valid_outcomes: projected.valid_outcomes,
1652 invalid_outcomes: projected.invalid_outcomes,
1653 results: projected.results.len(),
1654 proposed_record_drafts: projected.proposed_record_drafts.len(),
1655 };
1656 let error_cursor = AgenticMemoryCursor {
1657 evaluation,
1658 valid_records: 0,
1659 invalid_records: cursor.invalid_outcomes,
1660 projected_fragments: 0,
1661 result_count: cursor.results,
1662 };
1663 let errors = projected
1664 .errors
1665 .into_iter()
1666 .map(|error| AgenticMemoryError {
1667 code: error.code,
1668 message: "agentic_memory_consolidation_bundle: invalid consolidation input"
1669 .to_owned(),
1670 index: error.index,
1671 record_id: error.record_id,
1672 fragment_id: error.fragment_id,
1673 validation_errors: error.validation_errors,
1674 cursor: error_cursor.clone(),
1675 })
1676 .collect::<Vec<_>>();
1677 let status = AgenticMemoryConsolidationStatus {
1678 state: if errors.is_empty() && !projected.results.is_empty() {
1679 AgenticMemoryStatusState::Ready
1680 } else if errors.is_empty() {
1681 AgenticMemoryStatusState::Empty
1682 } else if !projected.results.is_empty() {
1683 AgenticMemoryStatusState::Partial
1684 } else {
1685 AgenticMemoryStatusState::Error
1686 },
1687 cursor: cursor.clone(),
1688 };
1689 ctx.state_set(evaluation);
1690 ctx.emit(AgenticMemoryConsolidationSnapshot {
1691 results: projected.results,
1692 proposed_record_drafts: projected.proposed_record_drafts,
1693 commands: projected.commands,
1694 status,
1695 errors,
1696 cursor,
1697 });
1698 },
1699 ),
1700 vec![requests.erased(), outcomes.erased()],
1701 named_solution_node_opts(format!("{name}/snapshot")),
1702 );
1703 AgenticMemoryConsolidationBundle {
1704 requests_input: requests,
1705 outcomes_input: outcomes,
1706 results: solution_projection(
1707 graph,
1708 &snapshot,
1709 format!("{name}/results"),
1710 "agenticMemoryConsolidationResults",
1711 |snapshot: &AgenticMemoryConsolidationSnapshot<T>| snapshot.results.clone(),
1712 ),
1713 proposed_record_drafts: solution_projection(
1714 graph,
1715 &snapshot,
1716 format!("{name}/proposed_record_drafts"),
1717 "agenticMemoryConsolidationRecordDrafts",
1718 |snapshot: &AgenticMemoryConsolidationSnapshot<T>| {
1719 snapshot.proposed_record_drafts.clone()
1720 },
1721 ),
1722 commands: solution_projection(
1723 graph,
1724 &snapshot,
1725 format!("{name}/commands"),
1726 "agenticMemoryConsolidationCommands",
1727 |snapshot: &AgenticMemoryConsolidationSnapshot<T>| snapshot.commands.clone(),
1728 ),
1729 status: solution_projection(
1730 graph,
1731 &snapshot,
1732 format!("{name}/status"),
1733 "agenticMemoryConsolidationStatus",
1734 |snapshot: &AgenticMemoryConsolidationSnapshot<T>| snapshot.status.clone(),
1735 ),
1736 errors: solution_projection(
1737 graph,
1738 &snapshot,
1739 format!("{name}/errors"),
1740 "agenticMemoryConsolidationErrors",
1741 |snapshot: &AgenticMemoryConsolidationSnapshot<T>| snapshot.errors.clone(),
1742 ),
1743 cursor: solution_projection(
1744 graph,
1745 &snapshot,
1746 format!("{name}/cursor"),
1747 "agenticMemoryConsolidationCursor",
1748 |snapshot: &AgenticMemoryConsolidationSnapshot<T>| snapshot.cursor.clone(),
1749 ),
1750 snapshot,
1751 }
1752}
1753
1754pub fn agentic_memory_context_packing_bundle<T: Clone + 'static>(
1756 graph: &Graph,
1757 opts: AgenticMemoryContextPackingBundleOptions<T>,
1758) -> AgenticMemoryContextPackingBundle<T> {
1759 let name = opts
1760 .name
1761 .unwrap_or_else(|| "agenticMemoryContextPacking".to_owned());
1762 let context = opts.context;
1763 let texts = opts.texts;
1764 let policy = opts.policy;
1765 let snapshot = graph.init_node(
1766 Operator::with_opts(
1767 "agenticMemoryContextPacking",
1768 solution_node_config(),
1769 |ctx| {
1770 let evaluation = ctx
1771 .state_get::<u64>()
1772 .map(|evaluation| *evaluation + 1)
1773 .unwrap_or(1);
1774 let context = ctx
1775 .data::<AgenticMemoryContext<T>>(0)
1776 .map(|context| (*context).clone());
1777 let text_facts = ctx
1778 .data::<Vec<AgenticMemoryTextProjection>>(1)
1779 .map(|texts| (*texts).clone())
1780 .unwrap_or_default();
1781 let policy = ctx
1782 .data::<AgenticMemoryContextPackingPolicy>(2)
1783 .map(|policy| (*policy).clone())
1784 .unwrap_or_default();
1785 let (packed_context, mut pending_errors, state) =
1786 pack_context(evaluation, context.as_ref(), &text_facts, &policy);
1787 let cursor = packed_context.cursor.clone();
1788 let status = AgenticMemoryContextPackingStatus {
1789 state,
1790 cursor: cursor.clone(),
1791 };
1792 ctx.state_set(evaluation);
1793 let errors = pending_errors
1794 .drain(..)
1795 .map(|error| AgenticMemoryError {
1796 code: error.code,
1797 message: "agentic_memory_context_packing_bundle: invalid packing input"
1798 .to_owned(),
1799 index: error.index,
1800 record_id: error.record_id,
1801 fragment_id: error.fragment_id,
1802 validation_errors: error.validation_errors,
1803 cursor: packing_error_cursor(&cursor),
1804 })
1805 .collect();
1806 ctx.emit(AgenticMemoryContextPackingSnapshot {
1807 packed_context,
1808 status,
1809 errors,
1810 cursor,
1811 });
1812 },
1813 ),
1814 vec![context.erased(), texts.erased(), policy.erased()],
1815 named_solution_node_opts(format!("{name}/snapshot")),
1816 );
1817
1818 AgenticMemoryContextPackingBundle {
1819 context_input: context,
1820 texts_input: texts,
1821 policy_input: policy,
1822 packed_context: solution_projection(
1823 graph,
1824 &snapshot,
1825 format!("{name}/packed_context"),
1826 "agenticMemoryPackedContext",
1827 |snapshot: &AgenticMemoryContextPackingSnapshot| snapshot.packed_context.clone(),
1828 ),
1829 status: solution_projection(
1830 graph,
1831 &snapshot,
1832 format!("{name}/status"),
1833 "agenticMemoryContextPackingStatus",
1834 |snapshot: &AgenticMemoryContextPackingSnapshot| snapshot.status.clone(),
1835 ),
1836 errors: solution_projection(
1837 graph,
1838 &snapshot,
1839 format!("{name}/errors"),
1840 "agenticMemoryContextPackingErrors",
1841 |snapshot: &AgenticMemoryContextPackingSnapshot| snapshot.errors.clone(),
1842 ),
1843 cursor: solution_projection(
1844 graph,
1845 &snapshot,
1846 format!("{name}/cursor"),
1847 "agenticMemoryContextPackingCursor",
1848 |snapshot: &AgenticMemoryContextPackingSnapshot| snapshot.cursor.clone(),
1849 ),
1850 snapshot,
1851 }
1852}
1853
1854fn agentic_projection<T, U, F>(
1855 graph: &Graph,
1856 projection: &Node<AgenticMemoryProjection<T>>,
1857 name: String,
1858 factory: &'static str,
1859 select: F,
1860) -> Node<U>
1861where
1862 T: Clone + 'static,
1863 U: 'static,
1864 F: Fn(&AgenticMemoryProjection<T>) -> U + 'static,
1865{
1866 graph.init_node(
1867 Operator::with_opts(factory, solution_node_config(), move |ctx| {
1868 for projection in ctx.batch::<AgenticMemoryProjection<T>>(0) {
1869 ctx.emit(select(projection.as_ref()));
1870 }
1871 }),
1872 vec![projection.erased()],
1873 named_solution_node_opts(name),
1874 )
1875}
1876
1877fn context_projection<T, U, F>(
1878 graph: &Graph,
1879 context: &Node<AgenticMemoryContext<T>>,
1880 name: String,
1881 factory: &'static str,
1882 select: F,
1883) -> Node<U>
1884where
1885 T: Clone + 'static,
1886 U: 'static,
1887 F: Fn(&AgenticMemoryContext<T>) -> U + 'static,
1888{
1889 graph.init_node(
1890 Operator::with_opts(factory, solution_node_config(), move |ctx| {
1891 for context in ctx.batch::<AgenticMemoryContext<T>>(0) {
1892 ctx.emit(select(context.as_ref()));
1893 }
1894 }),
1895 vec![context.erased()],
1896 named_solution_node_opts(name),
1897 )
1898}
1899
1900fn solution_projection<S, U, F>(
1901 graph: &Graph,
1902 snapshot: &Node<S>,
1903 name: String,
1904 factory: &'static str,
1905 select: F,
1906) -> Node<U>
1907where
1908 S: Clone + 'static,
1909 U: 'static,
1910 F: Fn(&S) -> U + 'static,
1911{
1912 graph.init_node(
1913 Operator::with_opts(factory, solution_node_config(), move |ctx| {
1914 for snapshot in ctx.batch::<S>(0) {
1915 ctx.emit(select(snapshot.as_ref()));
1916 }
1917 }),
1918 vec![snapshot.erased()],
1919 named_solution_node_opts(name),
1920 )
1921}
1922
1923fn solution_node_config() -> NodeOpts {
1924 NodeOpts {
1925 complete_when_deps_complete: false,
1926 error_when_deps_error: false,
1927 ..NodeOpts::default()
1928 }
1929}
1930
1931fn named_solution_node_opts(name: String) -> GraphNodeOpts {
1932 GraphNodeOpts {
1933 name: Some(name),
1934 ..GraphNodeOpts::default()
1935 }
1936}
1937
1938fn record_metadata<T>(record: &AgenticMemoryRecord<T>) -> AgenticMemoryRecordMetadata {
1939 AgenticMemoryRecordMetadata {
1940 record_id: record.id.clone(),
1941 kind: record.kind,
1942 persistence_level: record.persistence_level,
1943 artifact_kind: record.artifact_kind,
1944 scope: record.scope.clone(),
1945 }
1946}
1947
1948struct ValidAgenticRecords<T> {
1949 by_record_id: BTreeMap<FactId, AgenticMemoryRecord<T>>,
1950 in_order: Vec<AgenticMemoryRecord<T>>,
1951 errors: Vec<PendingAgenticMemoryError>,
1952}
1953
1954fn valid_record_index<T: Clone>(
1955 raw_records: Vec<AgenticMemoryRecord<T>>,
1956) -> ValidAgenticRecords<T> {
1957 let mut records = BTreeMap::new();
1958 let mut records_in_order = Vec::new();
1959 let mut seen_record_ids = HashSet::<FactId>::new();
1960 let mut seen_fragment_ids = HashSet::<FactId>::new();
1961 let mut pending_errors = Vec::new();
1962 for (index, record) in raw_records.into_iter().enumerate() {
1963 let validation = validate_agentic_memory_record(&record);
1964 if !validation.ok {
1965 pending_errors.push(PendingAgenticMemoryError {
1966 code: AgenticMemoryErrorCode::InvalidRecord,
1967 index: Some(index),
1968 record_id: Some(record.id.clone()),
1969 fragment_id: Some(record.fragment.id.clone()),
1970 validation_errors: validation.errors,
1971 });
1972 continue;
1973 }
1974 if !seen_record_ids.insert(record.id.clone()) {
1975 pending_errors.push(PendingAgenticMemoryError {
1976 code: AgenticMemoryErrorCode::DuplicateRecordId,
1977 index: Some(index),
1978 record_id: Some(record.id.clone()),
1979 fragment_id: Some(record.fragment.id.clone()),
1980 validation_errors: vec![format!("duplicate record id '{}'", record.id)],
1981 });
1982 continue;
1983 }
1984 if !seen_fragment_ids.insert(record.fragment.id.clone()) {
1985 pending_errors.push(PendingAgenticMemoryError {
1986 code: AgenticMemoryErrorCode::DuplicateFragmentId,
1987 index: Some(index),
1988 record_id: Some(record.id.clone()),
1989 fragment_id: Some(record.fragment.id.clone()),
1990 validation_errors: vec![format!("duplicate fragment id '{}'", record.fragment.id)],
1991 });
1992 continue;
1993 }
1994 records.insert(record.id.clone(), record.clone());
1995 records_in_order.push(record);
1996 }
1997 ValidAgenticRecords {
1998 by_record_id: records,
1999 in_order: records_in_order,
2000 errors: pending_errors,
2001 }
2002}
2003
2004fn validate_kg_draft(draft: &AgenticMemoryKgAssertionDraft) -> Vec<String> {
2005 let mut errors = Vec::new();
2006 if draft.id.is_empty() {
2007 errors.push("id must be a non-empty string".to_owned());
2008 }
2009 if draft.subject_id.is_empty() {
2010 errors.push("subject_id must be a non-empty string".to_owned());
2011 }
2012 if draft.predicate.is_empty() {
2013 errors.push("predicate must be a non-empty string".to_owned());
2014 }
2015 if !draft.confidence.is_finite() || !(0.0..=1.0).contains(&draft.confidence) {
2016 errors.push("confidence must be finite in [0, 1]".to_owned());
2017 }
2018 match &draft.object {
2019 KnowledgeAssertionObject::Entity { entity_id } if entity_id.is_empty() => {
2020 errors.push("object.entity_id must be a non-empty string".to_owned());
2021 }
2022 KnowledgeAssertionObject::Literal { value } => {
2023 if let Err(error) = validate_strict_json_value(value, "object.literal") {
2024 errors.push(error.to_string());
2025 }
2026 }
2027 KnowledgeAssertionObject::Entity { .. } => {}
2028 }
2029 errors
2030}
2031
2032fn validate_retention_command(command: &AgenticMemoryRetentionCommand) -> Vec<String> {
2033 let mut errors = Vec::new();
2034 if command.id.is_empty() {
2035 errors.push("id must be a non-empty string".to_owned());
2036 }
2037 if command.record_id.is_empty() {
2038 errors.push("record_id must be a non-empty string".to_owned());
2039 }
2040 if command
2041 .reason
2042 .as_ref()
2043 .is_some_and(|reason| reason.is_empty())
2044 {
2045 errors.push("reason must be non-empty when present".to_owned());
2046 }
2047 errors
2048}
2049
2050struct ProjectedConsolidation<T> {
2051 results: Vec<AgenticMemoryConsolidationResult>,
2052 proposed_record_drafts: Vec<AgenticMemoryConsolidationRecordDraft<T>>,
2053 commands: Vec<AgenticMemoryConsolidationCommand>,
2054 errors: Vec<PendingAgenticMemoryError>,
2055 valid_requests: usize,
2056 valid_outcomes: usize,
2057 invalid_outcomes: usize,
2058}
2059
2060fn project_consolidation_outcomes<T: Clone>(
2061 requests: Vec<AgenticMemoryConsolidationRequest>,
2062 outcomes: Vec<AgenticMemoryConsolidationOutcome<T>>,
2063) -> ProjectedConsolidation<T> {
2064 let mut by_request = BTreeMap::new();
2065 for request in requests {
2066 by_request.insert(request.command_id.clone(), request);
2067 }
2068 let valid_requests = by_request.len();
2069 let mut seen_outcomes = HashSet::new();
2070 let mut results = Vec::new();
2071 let mut proposed_record_drafts = Vec::new();
2072 let mut commands = Vec::new();
2073 let mut errors = Vec::new();
2074 let mut valid_outcomes = 0usize;
2075 let mut invalid_outcomes = 0usize;
2076 for (index, outcome) in outcomes.into_iter().enumerate() {
2077 let (outcome_id, request_id) = consolidation_outcome_ids(&outcome);
2078 let mut validation_errors = validate_consolidation_outcome(&outcome);
2079 if !seen_outcomes.insert(outcome_id.clone()) {
2080 validation_errors.push(format!("duplicate consolidation outcome id '{outcome_id}'"));
2081 }
2082 let request = by_request.get(&request_id);
2083 if request.is_none() {
2084 validation_errors.push(format!(
2085 "request_id '{request_id}' does not reference a projected request"
2086 ));
2087 }
2088 if !validation_errors.is_empty() {
2089 let code = if validation_errors
2090 .iter()
2091 .any(|error| error.starts_with("duplicate consolidation outcome id"))
2092 {
2093 AgenticMemoryErrorCode::DuplicateConsolidationOutcomeId
2094 } else if validation_errors
2095 .iter()
2096 .any(|error| error.starts_with("request_id"))
2097 {
2098 AgenticMemoryErrorCode::MissingConsolidationRequest
2099 } else {
2100 AgenticMemoryErrorCode::InvalidConsolidationOutcome
2101 };
2102 invalid_outcomes += 1;
2103 errors.push(PendingAgenticMemoryError {
2104 code,
2105 index: Some(index),
2106 record_id: Some(outcome_id),
2107 fragment_id: None,
2108 validation_errors,
2109 });
2110 continue;
2111 }
2112 let request = request.expect("validation checked request existence");
2113 valid_outcomes += 1;
2114 match outcome {
2115 AgenticMemoryConsolidationOutcome::Failed {
2116 id,
2117 request_id,
2118 message,
2119 provenance,
2120 } => {
2121 let result_id = format!("{request_id}:{id}");
2122 results.push(AgenticMemoryConsolidationResult {
2123 id: result_id.clone(),
2124 request_id: request_id.clone(),
2125 outcome_id: id.clone(),
2126 state: AgenticMemoryConsolidationResultState::Failed,
2127 source_record_ids: vec![request.record_id.clone()],
2128 proposed_record_ids: Vec::new(),
2129 message: Some(message.clone()),
2130 provenance,
2131 });
2132 commands.push(AgenticMemoryConsolidationCommand {
2133 id: format!("{result_id}:mark_failed"),
2134 kind: AgenticMemoryConsolidationCommandKind::MarkFailed,
2135 request_id,
2136 outcome_id: id,
2137 draft_ids: Vec::new(),
2138 message: Some(message),
2139 });
2140 }
2141 AgenticMemoryConsolidationOutcome::ProposedRecords {
2142 id,
2143 request_id,
2144 records,
2145 provenance,
2146 } => {
2147 let result_id = format!("{request_id}:{id}");
2148 let mut draft_ids = Vec::new();
2149 let mut proposed_record_ids = Vec::new();
2150 for record in records {
2151 let draft_id = format!("{request_id}:{id}:{}", record.id);
2152 draft_ids.push(draft_id.clone());
2153 proposed_record_ids.push(record.id.clone());
2154 proposed_record_drafts.push(AgenticMemoryConsolidationRecordDraft {
2155 id: draft_id,
2156 request_id: request_id.clone(),
2157 outcome_id: id.clone(),
2158 record,
2159 });
2160 }
2161 results.push(AgenticMemoryConsolidationResult {
2162 id: result_id.clone(),
2163 request_id: request_id.clone(),
2164 outcome_id: id.clone(),
2165 state: AgenticMemoryConsolidationResultState::Proposed,
2166 source_record_ids: vec![request.record_id.clone()],
2167 proposed_record_ids,
2168 message: None,
2169 provenance,
2170 });
2171 commands.push(AgenticMemoryConsolidationCommand {
2172 id: format!("{result_id}:propose_records"),
2173 kind: AgenticMemoryConsolidationCommandKind::ProposeRecords,
2174 request_id,
2175 outcome_id: id,
2176 draft_ids,
2177 message: None,
2178 });
2179 }
2180 }
2181 }
2182 ProjectedConsolidation {
2183 results,
2184 proposed_record_drafts,
2185 commands,
2186 errors,
2187 valid_requests,
2188 valid_outcomes,
2189 invalid_outcomes,
2190 }
2191}
2192
2193fn consolidation_outcome_ids<T>(
2194 outcome: &AgenticMemoryConsolidationOutcome<T>,
2195) -> (FactId, FactId) {
2196 match outcome {
2197 AgenticMemoryConsolidationOutcome::ProposedRecords { id, request_id, .. }
2198 | AgenticMemoryConsolidationOutcome::Failed { id, request_id, .. } => {
2199 (id.clone(), request_id.clone())
2200 }
2201 }
2202}
2203
2204fn validate_consolidation_outcome<T>(
2205 outcome: &AgenticMemoryConsolidationOutcome<T>,
2206) -> Vec<String> {
2207 let mut errors = Vec::new();
2208 match outcome {
2209 AgenticMemoryConsolidationOutcome::ProposedRecords {
2210 id,
2211 request_id,
2212 records,
2213 provenance,
2214 } => {
2215 if id.is_empty() {
2216 errors.push("id must be a non-empty string".to_owned());
2217 }
2218 if request_id.is_empty() {
2219 errors.push("request_id must be a non-empty string".to_owned());
2220 }
2221 if records.is_empty() {
2222 errors.push("records must be non-empty".to_owned());
2223 }
2224 if provenance.as_ref().is_some_and(|value| value.is_empty()) {
2225 errors.push("provenance must be non-empty when present".to_owned());
2226 }
2227 for (index, record) in records.iter().enumerate() {
2228 let validation = validate_agentic_memory_record(record);
2229 if !validation.ok {
2230 errors.extend(
2231 validation
2232 .errors
2233 .into_iter()
2234 .map(|error| format!("records[{index}]: {error}")),
2235 );
2236 }
2237 }
2238 }
2239 AgenticMemoryConsolidationOutcome::Failed {
2240 id,
2241 request_id,
2242 message,
2243 provenance,
2244 } => {
2245 if id.is_empty() {
2246 errors.push("id must be a non-empty string".to_owned());
2247 }
2248 if request_id.is_empty() {
2249 errors.push("request_id must be a non-empty string".to_owned());
2250 }
2251 if message.is_empty() {
2252 errors.push("message must be a non-empty string".to_owned());
2253 }
2254 if provenance.as_ref().is_some_and(|value| value.is_empty()) {
2255 errors.push("provenance must be non-empty when present".to_owned());
2256 }
2257 }
2258 }
2259 errors
2260}
2261
2262fn pack_context<T: Clone>(
2263 evaluation: u64,
2264 context: Option<&AgenticMemoryContext<T>>,
2265 text_facts: &[AgenticMemoryTextProjection],
2266 policy: &AgenticMemoryContextPackingPolicy,
2267) -> (
2268 AgenticMemoryPackedContext,
2269 Vec<PendingAgenticMemoryError>,
2270 AgenticMemoryStatusState,
2271) {
2272 let mut pending_errors = Vec::new();
2273 if policy.max_chars == Some(0) {
2274 pending_errors.push(PendingAgenticMemoryError {
2275 code: AgenticMemoryErrorCode::InvalidPackingPolicy,
2276 index: None,
2277 record_id: None,
2278 fragment_id: None,
2279 validation_errors: vec!["max_chars must be greater than 0 when present".to_owned()],
2280 });
2281 }
2282 let mut text_by_fragment = HashMap::<FactId, String>::new();
2283 let mut seen_text_fragment_ids = HashSet::<FactId>::new();
2284 for (index, text) in text_facts.iter().enumerate() {
2285 let mut validation_errors = Vec::new();
2286 if text.fragment_id.is_empty() {
2287 validation_errors.push("fragment_id must be a non-empty string".to_owned());
2288 }
2289 if text.text.is_empty() {
2290 validation_errors.push("text must be a non-empty string".to_owned());
2291 }
2292 if validation_errors.is_empty() && !seen_text_fragment_ids.insert(text.fragment_id.clone())
2293 {
2294 pending_errors.push(PendingAgenticMemoryError {
2295 code: AgenticMemoryErrorCode::DuplicateTextProjection,
2296 index: Some(index),
2297 record_id: None,
2298 fragment_id: Some(text.fragment_id.clone()),
2299 validation_errors: vec![format!(
2300 "duplicate text projection for fragment '{}'",
2301 text.fragment_id
2302 )],
2303 });
2304 continue;
2305 }
2306 if !validation_errors.is_empty() {
2307 pending_errors.push(PendingAgenticMemoryError {
2308 code: AgenticMemoryErrorCode::InvalidTextProjection,
2309 index: Some(index),
2310 record_id: None,
2311 fragment_id: if text.fragment_id.is_empty() {
2312 None
2313 } else {
2314 Some(text.fragment_id.clone())
2315 },
2316 validation_errors,
2317 });
2318 continue;
2319 }
2320 text_by_fragment.insert(text.fragment_id.clone(), text.text.clone());
2321 }
2322 let entries = context
2323 .map(|context| context.entries.as_slice())
2324 .unwrap_or(&[]);
2325 let mut packed_text = String::new();
2326 let mut fragment_ids = Vec::new();
2327 let mut missing_text = 0usize;
2328 let mut truncated = false;
2329 for (index, entry) in entries.iter().enumerate() {
2330 let Some(projected_text) = text_by_fragment.get(&entry.fragment_id) else {
2331 missing_text += 1;
2332 pending_errors.push(PendingAgenticMemoryError {
2333 code: AgenticMemoryErrorCode::MissingTextProjection,
2334 index: Some(index),
2335 record_id: None,
2336 fragment_id: Some(entry.fragment_id.clone()),
2337 validation_errors: vec![format!(
2338 "missing text projection for fragment '{}'",
2339 entry.fragment_id
2340 )],
2341 });
2342 continue;
2343 };
2344 let part = if policy.include_fragment_ids {
2345 format!("[{}] {projected_text}", entry.fragment_id)
2346 } else {
2347 projected_text.clone()
2348 };
2349 let addition = if packed_text.is_empty() {
2350 part
2351 } else {
2352 format!("{}{}", policy.separator, part)
2353 };
2354 if let Some(max_chars) = policy.max_chars {
2355 if packed_text.chars().count() + addition.chars().count() > max_chars {
2356 truncated = true;
2357 break;
2358 }
2359 }
2360 packed_text.push_str(&addition);
2361 fragment_ids.push(entry.fragment_id.clone());
2362 }
2363 let cursor = AgenticMemoryContextPackingCursor {
2364 evaluation,
2365 context_entries: entries.len(),
2366 text_projection_count: text_facts.len(),
2367 packed_entries: fragment_ids.len(),
2368 missing_text,
2369 char_count: packed_text.chars().count(),
2370 };
2371 let base_state = match context.map(|context| context.state) {
2372 Some(AgenticMemoryStatusState::Error) => AgenticMemoryStatusState::Error,
2373 _ if !pending_errors.is_empty() => AgenticMemoryStatusState::Partial,
2374 _ if truncated => AgenticMemoryStatusState::Partial,
2375 _ if fragment_ids.is_empty() => AgenticMemoryStatusState::Empty,
2376 _ => AgenticMemoryStatusState::Ready,
2377 };
2378 (
2379 AgenticMemoryPackedContext {
2380 text: packed_text,
2381 fragment_ids,
2382 truncated,
2383 cursor,
2384 },
2385 pending_errors,
2386 base_state,
2387 )
2388}
2389
2390fn projection_state(
2391 error_count: usize,
2392 output_count: usize,
2393 valid_input_count: usize,
2394) -> AgenticMemoryStatusState {
2395 if error_count > 0 && valid_input_count == 0 {
2396 AgenticMemoryStatusState::Error
2397 } else if error_count > 0 {
2398 AgenticMemoryStatusState::Partial
2399 } else if output_count > 0 {
2400 AgenticMemoryStatusState::Ready
2401 } else {
2402 AgenticMemoryStatusState::Empty
2403 }
2404}
2405
2406fn push_unique(values: &mut Vec<FactId>, value: FactId) {
2407 if !values.iter().any(|seen| seen == &value) {
2408 values.push(value);
2409 }
2410}
2411
2412fn kg_error_cursor(cursor: &AgenticMemoryKgProjectionCursor) -> AgenticMemoryCursor {
2413 AgenticMemoryCursor {
2414 evaluation: cursor.evaluation,
2415 valid_records: cursor.valid_records,
2416 invalid_records: cursor.invalid_drafts,
2417 projected_fragments: 0,
2418 result_count: cursor.projected_assertions,
2419 }
2420}
2421
2422fn retention_error_cursor(cursor: &AgenticMemoryRetentionCursor) -> AgenticMemoryCursor {
2423 AgenticMemoryCursor {
2424 evaluation: cursor.evaluation,
2425 valid_records: cursor.valid_records,
2426 invalid_records: cursor.invalid_commands,
2427 projected_fragments: cursor.active_records + cursor.archived_records,
2428 result_count: cursor.consolidation_requests,
2429 }
2430}
2431
2432fn packing_error_cursor(cursor: &AgenticMemoryContextPackingCursor) -> AgenticMemoryCursor {
2433 AgenticMemoryCursor {
2434 evaluation: cursor.evaluation,
2435 valid_records: cursor.context_entries.saturating_sub(cursor.missing_text),
2436 invalid_records: cursor.missing_text,
2437 projected_fragments: cursor.packed_entries,
2438 result_count: cursor.char_count,
2439 }
2440}
2441
2442impl Codec<AgenticMemoryRecordFrame> for AgenticMemoryRecordFrameCodec {
2443 fn encode(&self, frame: &AgenticMemoryRecordFrame) -> JsonCodecResult<Vec<u8>> {
2444 if frame.format != AGENTIC_MEMORY_RECORD_FRAME_FORMAT {
2445 return Err(JsonCodecError::validation(format!(
2446 "agenticMemoryRecordFrameCodec: format must be {AGENTIC_MEMORY_RECORD_FRAME_FORMAT}"
2447 )));
2448 }
2449 if frame.version != AGENTIC_MEMORY_RECORD_FRAME_VERSION {
2450 return Err(JsonCodecError::validation(format!(
2451 "agenticMemoryRecordFrameCodec: version must be {AGENTIC_MEMORY_RECORD_FRAME_VERSION}"
2452 )));
2453 }
2454 validate_agentic_memory_record(&frame.record)
2455 .errors
2456 .into_iter()
2457 .next()
2458 .map_or(Ok(()), |error| {
2459 Err(JsonCodecError::validation(format!(
2460 "agenticMemoryRecordFrameCodec: {error}"
2461 )))
2462 })?;
2463 validate_strict_json_value(&frame.record.fragment.payload, "record.fragment.payload")?;
2464 strict_canonical_json_bytes(&record_frame_to_json(frame)?)
2465 }
2466
2467 fn decode(&self, bytes: &[u8]) -> JsonCodecResult<AgenticMemoryRecordFrame> {
2468 let value = strict_json_decode(bytes)?;
2469 record_frame_from_json(&value)
2470 }
2471}
2472
2473fn record_frame_to_json(frame: &AgenticMemoryRecordFrame) -> JsonCodecResult<JsonValue> {
2474 let mut root = JsonMap::new();
2475 root.insert("format".to_owned(), JsonValue::String(frame.format.clone()));
2476 root.insert("record".to_owned(), record_to_json(&frame.record)?);
2477 root.insert(
2478 "version".to_owned(),
2479 JsonValue::Number(JsonNumber::from(frame.version)),
2480 );
2481 Ok(JsonValue::Object(root))
2482}
2483
2484fn record_to_json(record: &AgenticMemoryRecord<JsonValue>) -> JsonCodecResult<JsonValue> {
2485 let mut object = JsonMap::new();
2486 object.insert(
2487 "artifactKind".to_owned(),
2488 JsonValue::String(artifact_kind_to_str(record.artifact_kind).to_owned()),
2489 );
2490 object.insert("fragment".to_owned(), fragment_to_json(&record.fragment)?);
2491 object.insert("id".to_owned(), JsonValue::String(record.id.clone()));
2492 object.insert(
2493 "kind".to_owned(),
2494 JsonValue::String(memory_kind_to_str(record.kind).to_owned()),
2495 );
2496 object.insert(
2497 "persistenceLevel".to_owned(),
2498 JsonValue::String(persistence_level_to_str(record.persistence_level).to_owned()),
2499 );
2500 if let Some(scope) = &record.scope {
2501 object.insert("scope".to_owned(), scope_to_json(scope));
2502 }
2503 Ok(JsonValue::Object(object))
2504}
2505
2506fn fragment_to_json(fragment: &MemoryFragment<JsonValue>) -> JsonCodecResult<JsonValue> {
2507 let mut object = JsonMap::new();
2508 object.insert(
2509 "confidence".to_owned(),
2510 JsonValue::Number(finite_json_number(
2511 fragment.confidence,
2512 "agenticMemoryRecordFrameCodec: fragment.confidence",
2513 )?),
2514 );
2515 if let Some(embedding) = &fragment.embedding {
2516 object.insert(
2517 "embedding".to_owned(),
2518 JsonValue::Array(
2519 embedding
2520 .iter()
2521 .enumerate()
2522 .map(|(index, value)| {
2523 finite_json_number(
2524 *value,
2525 &format!("agenticMemoryRecordFrameCodec: fragment.embedding[{index}]"),
2526 )
2527 .map(JsonValue::Number)
2528 })
2529 .collect::<JsonCodecResult<Vec<_>>>()?,
2530 ),
2531 );
2532 }
2533 object.insert("id".to_owned(), JsonValue::String(fragment.id.clone()));
2534 if let Some(parent_fragment_id) = &fragment.parent_fragment_id {
2535 object.insert(
2536 "parentFragmentId".to_owned(),
2537 JsonValue::String(parent_fragment_id.clone()),
2538 );
2539 }
2540 object.insert("payload".to_owned(), fragment.payload.clone());
2541 if let Some(provenance) = &fragment.provenance {
2542 object.insert(
2543 "provenance".to_owned(),
2544 JsonValue::String(provenance.clone()),
2545 );
2546 }
2547 object.insert(
2548 "sources".to_owned(),
2549 JsonValue::Array(
2550 fragment
2551 .sources
2552 .iter()
2553 .cloned()
2554 .map(JsonValue::String)
2555 .collect(),
2556 ),
2557 );
2558 object.insert(
2559 "tags".to_owned(),
2560 JsonValue::Array(
2561 fragment
2562 .tags
2563 .iter()
2564 .cloned()
2565 .map(JsonValue::String)
2566 .collect(),
2567 ),
2568 );
2569 object.insert(
2570 "tNs".to_owned(),
2571 JsonValue::String(u128_to_non_negative_decimal_string(fragment.t_ns)),
2572 );
2573 if let Some(valid_from) = fragment.valid_from {
2574 object.insert(
2575 "validFrom".to_owned(),
2576 JsonValue::String(u128_to_non_negative_decimal_string(valid_from)),
2577 );
2578 }
2579 if let Some(valid_to) = fragment.valid_to {
2580 object.insert(
2581 "validTo".to_owned(),
2582 JsonValue::String(u128_to_non_negative_decimal_string(valid_to)),
2583 );
2584 }
2585 Ok(JsonValue::Object(object))
2586}
2587
2588fn finite_json_number(value: f64, label: &str) -> JsonCodecResult<JsonNumber> {
2589 JsonNumber::from_f64(value)
2590 .ok_or_else(|| JsonCodecError::validation(format!("{label} must be a finite number")))
2591}
2592
2593fn scope_to_json(scope: &AgenticMemoryScope) -> JsonValue {
2594 let mut object = JsonMap::new();
2595 if let Some(session_id) = &scope.session_id {
2596 object.insert(
2597 "sessionId".to_owned(),
2598 JsonValue::String(session_id.clone()),
2599 );
2600 }
2601 if let Some(project_id) = &scope.project_id {
2602 object.insert(
2603 "projectId".to_owned(),
2604 JsonValue::String(project_id.clone()),
2605 );
2606 }
2607 if let Some(user_id) = &scope.user_id {
2608 object.insert("userId".to_owned(), JsonValue::String(user_id.clone()));
2609 }
2610 if let Some(tenant_id) = &scope.tenant_id {
2611 object.insert("tenantId".to_owned(), JsonValue::String(tenant_id.clone()));
2612 }
2613 JsonValue::Object(object)
2614}
2615
2616fn record_frame_from_json(value: &JsonValue) -> JsonCodecResult<AgenticMemoryRecordFrame> {
2617 let object = as_object(value, "agenticMemoryRecordFrameCodec: frame")?;
2618 assert_known_keys(
2619 object,
2620 &["format", "record", "version"],
2621 "agenticMemoryRecordFrameCodec: frame",
2622 )?;
2623 let format = required_string(object, "format", "agenticMemoryRecordFrameCodec: format")?;
2624 if format != AGENTIC_MEMORY_RECORD_FRAME_FORMAT {
2625 return Err(JsonCodecError::validation(format!(
2626 "agenticMemoryRecordFrameCodec: format must be {AGENTIC_MEMORY_RECORD_FRAME_FORMAT}"
2627 )));
2628 }
2629 let version = required_u32(object, "version", "agenticMemoryRecordFrameCodec: version")?;
2630 if version != AGENTIC_MEMORY_RECORD_FRAME_VERSION {
2631 return Err(JsonCodecError::validation(format!(
2632 "agenticMemoryRecordFrameCodec: version must be {AGENTIC_MEMORY_RECORD_FRAME_VERSION}"
2633 )));
2634 }
2635 let record = record_from_json(required_value(
2636 object,
2637 "record",
2638 "agenticMemoryRecordFrameCodec: record",
2639 )?)?;
2640 let frame = AgenticMemoryRecordFrame {
2641 format,
2642 version,
2643 record,
2644 };
2645 let validation = validate_agentic_memory_record(&frame.record);
2646 if !validation.ok {
2647 return Err(JsonCodecError::validation(format!(
2648 "agenticMemoryRecordFrameCodec: {}",
2649 validation.errors.join("; ")
2650 )));
2651 }
2652 validate_strict_json_value(&frame.record.fragment.payload, "record.fragment.payload")?;
2653 Ok(frame)
2654}
2655
2656fn record_from_json(value: &JsonValue) -> JsonCodecResult<AgenticMemoryRecord<JsonValue>> {
2657 let object = as_object(value, "agenticMemoryRecordFrameCodec: record")?;
2658 assert_known_keys(
2659 object,
2660 &[
2661 "artifactKind",
2662 "fragment",
2663 "id",
2664 "kind",
2665 "persistenceLevel",
2666 "scope",
2667 ],
2668 "agenticMemoryRecordFrameCodec: record",
2669 )?;
2670 Ok(AgenticMemoryRecord {
2671 id: required_string(object, "id", "agenticMemoryRecordFrameCodec: record.id")?,
2672 kind: memory_kind_from_str(&required_string(
2673 object,
2674 "kind",
2675 "agenticMemoryRecordFrameCodec: record.kind",
2676 )?)?,
2677 persistence_level: persistence_level_from_str(&required_string(
2678 object,
2679 "persistenceLevel",
2680 "agenticMemoryRecordFrameCodec: record.persistenceLevel",
2681 )?)?,
2682 artifact_kind: artifact_kind_from_str(&required_string(
2683 object,
2684 "artifactKind",
2685 "agenticMemoryRecordFrameCodec: record.artifactKind",
2686 )?)?,
2687 scope: optional_scope(object.get("scope"))?,
2688 fragment: fragment_from_json(required_value(
2689 object,
2690 "fragment",
2691 "agenticMemoryRecordFrameCodec: record.fragment",
2692 )?)?,
2693 })
2694}
2695
2696fn fragment_from_json(value: &JsonValue) -> JsonCodecResult<MemoryFragment<JsonValue>> {
2697 let object = as_object(value, "agenticMemoryRecordFrameCodec: fragment")?;
2698 assert_known_keys(
2699 object,
2700 &[
2701 "confidence",
2702 "embedding",
2703 "id",
2704 "parentFragmentId",
2705 "payload",
2706 "provenance",
2707 "sources",
2708 "tags",
2709 "tNs",
2710 "validFrom",
2711 "validTo",
2712 ],
2713 "agenticMemoryRecordFrameCodec: fragment",
2714 )?;
2715 Ok(MemoryFragment {
2716 id: required_string(object, "id", "agenticMemoryRecordFrameCodec: fragment.id")?,
2717 payload: required_value(
2718 object,
2719 "payload",
2720 "agenticMemoryRecordFrameCodec: fragment.payload",
2721 )?
2722 .clone(),
2723 t_ns: required_decimal_u128(object, "tNs", "agenticMemoryRecordFrameCodec: fragment.tNs")?,
2724 valid_from: optional_decimal_u128(
2725 object,
2726 "validFrom",
2727 "agenticMemoryRecordFrameCodec: fragment.validFrom",
2728 )?,
2729 valid_to: optional_decimal_u128(
2730 object,
2731 "validTo",
2732 "agenticMemoryRecordFrameCodec: fragment.validTo",
2733 )?,
2734 confidence: required_f64(
2735 object,
2736 "confidence",
2737 "agenticMemoryRecordFrameCodec: fragment.confidence",
2738 )?,
2739 tags: required_string_array(
2740 object,
2741 "tags",
2742 "agenticMemoryRecordFrameCodec: fragment.tags",
2743 )?,
2744 sources: required_string_array(
2745 object,
2746 "sources",
2747 "agenticMemoryRecordFrameCodec: fragment.sources",
2748 )?,
2749 embedding: optional_f64_array(
2750 object,
2751 "embedding",
2752 "agenticMemoryRecordFrameCodec: fragment.embedding",
2753 )?,
2754 parent_fragment_id: optional_string(
2755 object,
2756 "parentFragmentId",
2757 "agenticMemoryRecordFrameCodec: fragment.parentFragmentId",
2758 )?,
2759 provenance: optional_string(
2760 object,
2761 "provenance",
2762 "agenticMemoryRecordFrameCodec: fragment.provenance",
2763 )?,
2764 })
2765}
2766
2767fn optional_scope(value: Option<&JsonValue>) -> JsonCodecResult<Option<AgenticMemoryScope>> {
2768 let Some(value) = value else {
2769 return Ok(None);
2770 };
2771 let object = as_object(value, "agenticMemoryRecordFrameCodec: scope")?;
2772 assert_known_keys(
2773 object,
2774 &["projectId", "sessionId", "tenantId", "userId"],
2775 "agenticMemoryRecordFrameCodec: scope",
2776 )?;
2777 Ok(Some(AgenticMemoryScope {
2778 session_id: optional_string(
2779 object,
2780 "sessionId",
2781 "agenticMemoryRecordFrameCodec: scope.sessionId",
2782 )?,
2783 project_id: optional_string(
2784 object,
2785 "projectId",
2786 "agenticMemoryRecordFrameCodec: scope.projectId",
2787 )?,
2788 user_id: optional_string(
2789 object,
2790 "userId",
2791 "agenticMemoryRecordFrameCodec: scope.userId",
2792 )?,
2793 tenant_id: optional_string(
2794 object,
2795 "tenantId",
2796 "agenticMemoryRecordFrameCodec: scope.tenantId",
2797 )?,
2798 }))
2799}
2800
2801fn as_object<'a>(
2802 value: &'a JsonValue,
2803 label: &str,
2804) -> JsonCodecResult<&'a JsonMap<String, JsonValue>> {
2805 value
2806 .as_object()
2807 .ok_or_else(|| JsonCodecError::validation(format!("{label} must be an object")))
2808}
2809
2810fn assert_known_keys(
2811 object: &JsonMap<String, JsonValue>,
2812 allowed: &[&str],
2813 label: &str,
2814) -> JsonCodecResult<()> {
2815 for key in object.keys() {
2816 if !allowed.iter().any(|allowed| allowed == key) {
2817 return Err(JsonCodecError::validation(format!(
2818 "{label}: unknown field {key}"
2819 )));
2820 }
2821 }
2822 Ok(())
2823}
2824
2825fn required_value<'a>(
2826 object: &'a JsonMap<String, JsonValue>,
2827 key: &str,
2828 label: &str,
2829) -> JsonCodecResult<&'a JsonValue> {
2830 object
2831 .get(key)
2832 .ok_or_else(|| JsonCodecError::validation(format!("{label} is required")))
2833}
2834
2835fn required_string(
2836 object: &JsonMap<String, JsonValue>,
2837 key: &str,
2838 label: &str,
2839) -> JsonCodecResult<String> {
2840 required_value(object, key, label)?
2841 .as_str()
2842 .map(str::to_owned)
2843 .ok_or_else(|| JsonCodecError::validation(format!("{label} must be a string")))
2844}
2845
2846fn optional_string(
2847 object: &JsonMap<String, JsonValue>,
2848 key: &str,
2849 label: &str,
2850) -> JsonCodecResult<Option<String>> {
2851 object
2852 .get(key)
2853 .map(|value| {
2854 value
2855 .as_str()
2856 .map(str::to_owned)
2857 .ok_or_else(|| JsonCodecError::validation(format!("{label} must be a string")))
2858 })
2859 .transpose()
2860}
2861
2862fn required_u32(
2863 object: &JsonMap<String, JsonValue>,
2864 key: &str,
2865 label: &str,
2866) -> JsonCodecResult<u32> {
2867 let value = required_value(object, key, label)?
2868 .as_u64()
2869 .ok_or_else(|| {
2870 JsonCodecError::validation(format!("{label} must be a non-negative integer"))
2871 })?;
2872 u32::try_from(value)
2873 .map_err(|err| JsonCodecError::validation(format!("{label} is outside u32 range: {err}")))
2874}
2875
2876fn required_f64(
2877 object: &JsonMap<String, JsonValue>,
2878 key: &str,
2879 label: &str,
2880) -> JsonCodecResult<f64> {
2881 required_value(object, key, label)?
2882 .as_f64()
2883 .filter(|value| value.is_finite())
2884 .ok_or_else(|| JsonCodecError::validation(format!("{label} must be a finite number")))
2885}
2886
2887fn required_decimal_u128(
2888 object: &JsonMap<String, JsonValue>,
2889 key: &str,
2890 label: &str,
2891) -> JsonCodecResult<u128> {
2892 non_negative_decimal_string_to_u128(&required_string(object, key, label)?)
2893}
2894
2895fn optional_decimal_u128(
2896 object: &JsonMap<String, JsonValue>,
2897 key: &str,
2898 label: &str,
2899) -> JsonCodecResult<Option<u128>> {
2900 optional_string(object, key, label)?
2901 .map(|value| non_negative_decimal_string_to_u128(&value))
2902 .transpose()
2903}
2904
2905fn required_string_array(
2906 object: &JsonMap<String, JsonValue>,
2907 key: &str,
2908 label: &str,
2909) -> JsonCodecResult<Vec<String>> {
2910 required_value(object, key, label)?
2911 .as_array()
2912 .ok_or_else(|| JsonCodecError::validation(format!("{label} must be an array")))?
2913 .iter()
2914 .enumerate()
2915 .map(|(index, value)| {
2916 value.as_str().map(str::to_owned).ok_or_else(|| {
2917 JsonCodecError::validation(format!("{label}[{index}] must be a string"))
2918 })
2919 })
2920 .collect()
2921}
2922
2923fn optional_f64_array(
2924 object: &JsonMap<String, JsonValue>,
2925 key: &str,
2926 label: &str,
2927) -> JsonCodecResult<Option<Vec<f64>>> {
2928 object
2929 .get(key)
2930 .map(|value| {
2931 value
2932 .as_array()
2933 .ok_or_else(|| JsonCodecError::validation(format!("{label} must be an array")))?
2934 .iter()
2935 .enumerate()
2936 .map(|(index, value)| {
2937 value
2938 .as_f64()
2939 .filter(|value| value.is_finite())
2940 .ok_or_else(|| {
2941 JsonCodecError::validation(format!(
2942 "{label}[{index}] must be a finite number"
2943 ))
2944 })
2945 })
2946 .collect()
2947 })
2948 .transpose()
2949}
2950
2951fn memory_kind_to_str(kind: AgenticMemoryKind) -> &'static str {
2952 match kind {
2953 AgenticMemoryKind::Working => "working",
2954 AgenticMemoryKind::Episodic => "episodic",
2955 AgenticMemoryKind::Semantic => "semantic",
2956 AgenticMemoryKind::Procedural => "procedural",
2957 AgenticMemoryKind::Profile => "profile",
2958 }
2959}
2960
2961fn memory_kind_from_str(value: &str) -> JsonCodecResult<AgenticMemoryKind> {
2962 match value {
2963 "working" => Ok(AgenticMemoryKind::Working),
2964 "episodic" => Ok(AgenticMemoryKind::Episodic),
2965 "semantic" => Ok(AgenticMemoryKind::Semantic),
2966 "procedural" => Ok(AgenticMemoryKind::Procedural),
2967 "profile" => Ok(AgenticMemoryKind::Profile),
2968 _ => Err(JsonCodecError::validation(format!(
2969 "agenticMemoryRecordFrameCodec: invalid memory kind '{value}'"
2970 ))),
2971 }
2972}
2973
2974fn persistence_level_to_str(level: AgenticMemoryPersistenceLevel) -> &'static str {
2975 match level {
2976 AgenticMemoryPersistenceLevel::Turn => "turn",
2977 AgenticMemoryPersistenceLevel::Session => "session",
2978 AgenticMemoryPersistenceLevel::Project => "project",
2979 AgenticMemoryPersistenceLevel::LongTerm => "longTerm",
2980 AgenticMemoryPersistenceLevel::Permanent => "permanent",
2981 AgenticMemoryPersistenceLevel::Archived => "archived",
2982 }
2983}
2984
2985fn persistence_level_from_str(value: &str) -> JsonCodecResult<AgenticMemoryPersistenceLevel> {
2986 match value {
2987 "turn" => Ok(AgenticMemoryPersistenceLevel::Turn),
2988 "session" => Ok(AgenticMemoryPersistenceLevel::Session),
2989 "project" => Ok(AgenticMemoryPersistenceLevel::Project),
2990 "longTerm" => Ok(AgenticMemoryPersistenceLevel::LongTerm),
2991 "permanent" => Ok(AgenticMemoryPersistenceLevel::Permanent),
2992 "archived" => Ok(AgenticMemoryPersistenceLevel::Archived),
2993 _ => Err(JsonCodecError::validation(format!(
2994 "agenticMemoryRecordFrameCodec: invalid persistence level '{value}'"
2995 ))),
2996 }
2997}
2998
2999fn artifact_kind_to_str(kind: AgenticMemoryArtifactKind) -> &'static str {
3000 match kind {
3001 AgenticMemoryArtifactKind::Raw => "raw",
3002 AgenticMemoryArtifactKind::Insight => "insight",
3003 AgenticMemoryArtifactKind::Profile => "profile",
3004 AgenticMemoryArtifactKind::Procedure => "procedure",
3005 }
3006}
3007
3008fn artifact_kind_from_str(value: &str) -> JsonCodecResult<AgenticMemoryArtifactKind> {
3009 match value {
3010 "raw" => Ok(AgenticMemoryArtifactKind::Raw),
3011 "insight" => Ok(AgenticMemoryArtifactKind::Insight),
3012 "profile" => Ok(AgenticMemoryArtifactKind::Profile),
3013 "procedure" => Ok(AgenticMemoryArtifactKind::Procedure),
3014 _ => Err(JsonCodecError::validation(format!(
3015 "agenticMemoryRecordFrameCodec: invalid artifact kind '{value}'"
3016 ))),
3017 }
3018}
3019
3020fn context_from_snapshot<T: Clone>(
3021 projection: &AgenticMemoryProjection<T>,
3022 snapshot: &MemoryRetrievalSnapshot<T>,
3023) -> AgenticMemoryContext<T> {
3024 let cursor = AgenticMemoryCursor {
3025 evaluation: snapshot.cursor.evaluation,
3026 valid_records: projection.cursor.valid_records,
3027 invalid_records: projection.cursor.invalid_records,
3028 projected_fragments: projection.cursor.projected_fragments,
3029 result_count: snapshot.cursor.result_count,
3030 };
3031 let errors = projection.errors.clone();
3032 let state = agentic_status_state(projection, snapshot);
3033 let entries = snapshot
3034 .ranked
3035 .results
3036 .iter()
3037 .map(|fragment| AgenticMemoryContextEntry {
3038 fragment_id: fragment.id.clone(),
3039 payload: fragment.payload.clone(),
3040 confidence: fragment.confidence,
3041 tags: fragment.tags.clone(),
3042 sources: fragment.sources.clone(),
3043 fragment: fragment.clone(),
3044 metadata: projection
3045 .metadata_by_fragment_id
3046 .get(&fragment.id)
3047 .cloned(),
3048 })
3049 .collect::<Vec<_>>();
3050 let context_ready = !entries.is_empty()
3051 && matches!(
3052 state,
3053 AgenticMemoryStatusState::Ready | AgenticMemoryStatusState::Partial
3054 );
3055 AgenticMemoryContext {
3056 state,
3057 query: snapshot.ranked.query.clone(),
3058 entries,
3059 cursor,
3060 errors,
3061 retrieval_status: snapshot.status.clone(),
3062 retrieval_errors: snapshot.errors.clone(),
3063 context_ready,
3064 }
3065}
3066
3067fn agentic_status_state<T>(
3068 projection: &AgenticMemoryProjection<T>,
3069 snapshot: &MemoryRetrievalSnapshot<T>,
3070) -> AgenticMemoryStatusState {
3071 if snapshot.status.state == MemoryRetrievalStatusState::Error {
3072 return AgenticMemoryStatusState::Error;
3073 }
3074 if !projection.errors.is_empty() && projection.cursor.valid_records == 0 {
3075 return AgenticMemoryStatusState::Error;
3076 }
3077 if !projection.errors.is_empty() || snapshot.status.state == MemoryRetrievalStatusState::Partial
3078 {
3079 return AgenticMemoryStatusState::Partial;
3080 }
3081 if snapshot.cursor.result_count > 0 {
3082 AgenticMemoryStatusState::Ready
3083 } else {
3084 AgenticMemoryStatusState::Empty
3085 }
3086}
3087
3088#[derive(Clone, Debug)]
3089struct PendingAgenticMemoryError {
3090 code: AgenticMemoryErrorCode,
3091 index: Option<usize>,
3092 record_id: Option<FactId>,
3093 fragment_id: Option<FactId>,
3094 validation_errors: Vec<String>,
3095}