Skip to main content

graphrefly/
solutions.rs

1//! Vertical graph-visible solutions.
2//!
3//! D164 keeps the agentic-memory record envelope at solution level. Lower
4//! semantic-memory patterns still consume projected [`MemoryFragment`] facts.
5
6use 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
24/// `constant` constant.
25pub const AGENTIC_MEMORY_RECORD_FRAME_FORMAT: &str = "graphrefly.agenticMemoryRecord";
26/// `constant` constant.
27pub const AGENTIC_MEMORY_RECORD_FRAME_VERSION: u32 = 1;
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30/// `AgenticMemoryKind` variants.
31pub enum AgenticMemoryKind {
32    /// `Working` variant.
33    Working,
34    /// `Episodic` variant.
35    Episodic,
36    /// `Semantic` variant.
37    Semantic,
38    /// `Procedural` variant.
39    Procedural,
40    /// `Profile` variant.
41    Profile,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45/// `AgenticMemoryPersistenceLevel` variants.
46pub enum AgenticMemoryPersistenceLevel {
47    /// `Turn` variant.
48    Turn,
49    /// `Session` variant.
50    Session,
51    /// `Project` variant.
52    Project,
53    /// `LongTerm` variant.
54    LongTerm,
55    /// `Permanent` variant.
56    Permanent,
57    /// `Archived` variant.
58    Archived,
59}
60
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62/// `AgenticMemoryArtifactKind` variants.
63pub enum AgenticMemoryArtifactKind {
64    /// `Raw` variant.
65    Raw,
66    /// `Insight` variant.
67    Insight,
68    /// `Profile` variant.
69    Profile,
70    /// `Procedure` variant.
71    Procedure,
72}
73
74#[derive(Clone, Debug, Default, PartialEq, Eq)]
75/// `AgenticMemoryScope` data container.
76pub struct AgenticMemoryScope {
77    /// `session_id` field for session id.
78    pub session_id: Option<String>,
79    /// `project_id` field for project id.
80    pub project_id: Option<String>,
81    /// `user_id` field for user id.
82    pub user_id: Option<String>,
83    /// `tenant_id` field for tenant id.
84    pub tenant_id: Option<String>,
85}
86
87#[derive(Clone, Debug, PartialEq)]
88/// `AgenticMemoryRecord` data container.
89pub struct AgenticMemoryRecord<T> {
90    /// `id` field for id.
91    pub id: FactId,
92    /// `fragment` field for fragment.
93    pub fragment: MemoryFragment<T>,
94    /// `kind` field for kind.
95    pub kind: AgenticMemoryKind,
96    /// `persistence_level` field for persistence level.
97    pub persistence_level: AgenticMemoryPersistenceLevel,
98    /// `artifact_kind` field for artifact kind.
99    pub artifact_kind: AgenticMemoryArtifactKind,
100    /// `scope` field for scope.
101    pub scope: Option<AgenticMemoryScope>,
102}
103
104#[derive(Clone, Debug, PartialEq, Eq)]
105/// `AgenticMemoryFieldValidation` data container.
106pub struct AgenticMemoryFieldValidation {
107    /// `ok` field for ok.
108    pub ok: bool,
109    /// `errors` field for errors.
110    pub errors: Vec<String>,
111}
112
113#[derive(Clone, Debug, PartialEq, Eq)]
114/// `AgenticMemoryRecordValidation` data container.
115pub struct AgenticMemoryRecordValidation {
116    /// `ok` field for ok.
117    pub ok: bool,
118    /// `errors` field for errors.
119    pub errors: Vec<String>,
120}
121
122#[derive(Clone, Debug, PartialEq, Eq)]
123/// `AgenticMemoryRecordMetadata` data container.
124pub struct AgenticMemoryRecordMetadata {
125    /// `record_id` field for record id.
126    pub record_id: FactId,
127    /// `kind` field for kind.
128    pub kind: AgenticMemoryKind,
129    /// `persistence_level` field for persistence level.
130    pub persistence_level: AgenticMemoryPersistenceLevel,
131    /// `artifact_kind` field for artifact kind.
132    pub artifact_kind: AgenticMemoryArtifactKind,
133    /// `scope` field for scope.
134    pub scope: Option<AgenticMemoryScope>,
135}
136
137#[derive(Clone, Debug, PartialEq)]
138/// `AgenticMemorySourceProjection` data container.
139pub struct AgenticMemorySourceProjection {
140    /// `fragment_id` field for fragment id.
141    pub fragment_id: FactId,
142    /// `sources` field for sources.
143    pub sources: Vec<FactId>,
144    /// `parent_fragment_id` field for parent fragment id.
145    pub parent_fragment_id: Option<FactId>,
146    /// `provenance` field for provenance.
147    pub provenance: Option<String>,
148    /// `metadata` field for metadata.
149    pub metadata: AgenticMemoryRecordMetadata,
150}
151
152#[derive(Clone, Copy, Debug, PartialEq, Eq)]
153/// `AgenticMemoryStatusState` variants.
154pub enum AgenticMemoryStatusState {
155    /// `Ready` variant.
156    Ready,
157    /// `Empty` variant.
158    Empty,
159    /// `Partial` variant.
160    Partial,
161    /// `Error` variant.
162    Error,
163}
164
165#[derive(Clone, Debug, PartialEq, Eq)]
166/// `AgenticMemoryCursor` data container.
167pub struct AgenticMemoryCursor {
168    /// `evaluation` field for evaluation.
169    pub evaluation: u64,
170    /// `valid_records` field for valid records.
171    pub valid_records: usize,
172    /// `invalid_records` field for invalid records.
173    pub invalid_records: usize,
174    /// `projected_fragments` field for projected fragments.
175    pub projected_fragments: usize,
176    /// `result_count` field for result count.
177    pub result_count: usize,
178}
179
180#[derive(Clone, Debug, PartialEq)]
181/// `AgenticMemoryStatus` data container.
182pub struct AgenticMemoryStatus {
183    /// `state` field for state.
184    pub state: AgenticMemoryStatusState,
185    /// `query` field for query.
186    pub query: MemoryRetrievalQuery,
187    /// `cursor` field for cursor.
188    pub cursor: AgenticMemoryCursor,
189    /// `retrieval_status` field for retrieval status.
190    pub retrieval_status: MemoryRetrievalStatus,
191}
192
193#[derive(Clone, Copy, Debug, PartialEq, Eq)]
194/// `AgenticMemoryErrorCode` variants.
195pub enum AgenticMemoryErrorCode {
196    /// `DuplicateRecordId` variant.
197    DuplicateRecordId,
198    /// `DuplicateFragmentId` variant.
199    DuplicateFragmentId,
200    /// `InvalidRecord` variant.
201    InvalidRecord,
202    /// `InvalidScope` variant.
203    InvalidScope,
204    /// `InvalidFragment` variant.
205    InvalidFragment,
206    /// `InvalidKgDraft` variant.
207    InvalidKgDraft,
208    /// `DuplicateAssertionId` variant.
209    DuplicateAssertionId,
210    /// `InvalidRetentionCommand` variant.
211    InvalidRetentionCommand,
212    /// `DuplicateRetentionCommandId` variant.
213    DuplicateRetentionCommandId,
214    /// `InvalidConsolidationOutcome` variant.
215    InvalidConsolidationOutcome,
216    /// `DuplicateConsolidationOutcomeId` variant.
217    DuplicateConsolidationOutcomeId,
218    /// `MissingConsolidationRequest` variant.
219    MissingConsolidationRequest,
220    /// `InvalidPackingPolicy` variant.
221    InvalidPackingPolicy,
222    /// `InvalidTextProjection` variant.
223    InvalidTextProjection,
224    /// `DuplicateTextProjection` variant.
225    DuplicateTextProjection,
226    /// `MissingTextProjection` variant.
227    MissingTextProjection,
228}
229
230#[derive(Clone, Debug, PartialEq, Eq)]
231/// `AgenticMemoryError` data container.
232pub struct AgenticMemoryError {
233    /// `code` field for code.
234    pub code: AgenticMemoryErrorCode,
235    /// `message` field for message.
236    pub message: String,
237    /// `index` field for index.
238    pub index: Option<usize>,
239    /// `record_id` field for record id.
240    pub record_id: Option<FactId>,
241    /// `fragment_id` field for fragment id.
242    pub fragment_id: Option<FactId>,
243    /// `validation_errors` field for validation errors.
244    pub validation_errors: Vec<String>,
245    /// `cursor` field for cursor.
246    pub cursor: AgenticMemoryCursor,
247}
248
249#[derive(Clone, Debug, PartialEq)]
250/// `AgenticMemoryRecordFrame` data container.
251pub struct AgenticMemoryRecordFrame {
252    /// `format` field for format.
253    pub format: String,
254    /// `version` field for version.
255    pub version: u32,
256    /// `record` field for record.
257    pub record: AgenticMemoryRecord<JsonValue>,
258}
259
260#[derive(Clone, Copy, Debug, Default)]
261/// `AgenticMemoryRecordFrameCodec` data container.
262pub struct AgenticMemoryRecordFrameCodec;
263
264/// Creates or computes `agentic_memory_record_frame`.
265pub 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
275/// Creates or computes `agentic_memory_record_frame_codec`.
276pub fn agentic_memory_record_frame_codec() -> AgenticMemoryRecordFrameCodec {
277    AgenticMemoryRecordFrameCodec
278}
279
280#[derive(Clone, Debug, PartialEq)]
281/// `AgenticMemoryKgAssertionDraft` data container.
282pub struct AgenticMemoryKgAssertionDraft {
283    /// `id` field for id.
284    pub id: FactId,
285    /// `record_id` field for record id.
286    pub record_id: Option<FactId>,
287    /// `fragment_id` field for fragment id.
288    pub fragment_id: Option<FactId>,
289    /// `subject_id` field for subject id.
290    pub subject_id: FactId,
291    /// `predicate` field for predicate.
292    pub predicate: String,
293    /// `object` field for object.
294    pub object: KnowledgeAssertionObject,
295    /// `confidence` field for confidence.
296    pub confidence: f64,
297    /// `t_ns` field for t ns.
298    pub t_ns: u128,
299}
300
301#[derive(Clone, Debug, PartialEq, Eq)]
302/// `AgenticMemoryKgProjectionCursor` data container.
303pub struct AgenticMemoryKgProjectionCursor {
304    /// `evaluation` field for evaluation.
305    pub evaluation: u64,
306    /// `valid_records` field for valid records.
307    pub valid_records: usize,
308    /// `valid_drafts` field for valid drafts.
309    pub valid_drafts: usize,
310    /// `invalid_drafts` field for invalid drafts.
311    pub invalid_drafts: usize,
312    /// `projected_assertions` field for projected assertions.
313    pub projected_assertions: usize,
314}
315
316#[derive(Clone, Debug, PartialEq)]
317/// `AgenticMemoryKgProjectionStatus` data container.
318pub struct AgenticMemoryKgProjectionStatus {
319    /// `state` field for state.
320    pub state: AgenticMemoryStatusState,
321    /// `cursor` field for cursor.
322    pub cursor: AgenticMemoryKgProjectionCursor,
323}
324
325#[derive(Clone, Debug, PartialEq)]
326/// `AgenticMemoryKgProjectionSnapshot` data container.
327pub struct AgenticMemoryKgProjectionSnapshot {
328    /// `assertions` field for assertions.
329    pub assertions: Vec<KnowledgeAssertion>,
330    /// `status` field for status.
331    pub status: AgenticMemoryKgProjectionStatus,
332    /// `errors` field for errors.
333    pub errors: Vec<AgenticMemoryError>,
334    /// `cursor` field for cursor.
335    pub cursor: AgenticMemoryKgProjectionCursor,
336}
337
338#[derive(Clone)]
339/// `AgenticMemoryKgProjectionBundleOptions` data container.
340pub struct AgenticMemoryKgProjectionBundleOptions<T> {
341    /// `name` field for name.
342    pub name: Option<String>,
343    /// `records` field for records.
344    pub records: Node<Vec<AgenticMemoryRecord<T>>>,
345    /// `drafts` field for drafts.
346    pub drafts: Node<Vec<AgenticMemoryKgAssertionDraft>>,
347}
348
349impl<T> AgenticMemoryKgProjectionBundleOptions<T> {
350    /// Creates or computes `new`.
351    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    /// Updates or reads `named`.
363    pub fn named(mut self, name: impl Into<String>) -> Self {
364        self.name = Some(name.into());
365        self
366    }
367}
368
369#[derive(Clone)]
370/// `AgenticMemoryKgProjectionBundle` data container.
371pub struct AgenticMemoryKgProjectionBundle<T> {
372    /// `records_input` field for records input.
373    pub records_input: Node<Vec<AgenticMemoryRecord<T>>>,
374    /// `drafts_input` field for drafts input.
375    pub drafts_input: Node<Vec<AgenticMemoryKgAssertionDraft>>,
376    /// `snapshot` field for snapshot.
377    pub snapshot: Node<AgenticMemoryKgProjectionSnapshot>,
378    /// `assertions` field for assertions.
379    pub assertions: Node<Vec<KnowledgeAssertion>>,
380    /// `status` field for status.
381    pub status: Node<AgenticMemoryKgProjectionStatus>,
382    /// `errors` field for errors.
383    pub errors: Node<Vec<AgenticMemoryError>>,
384    /// `cursor` field for cursor.
385    pub cursor: Node<AgenticMemoryKgProjectionCursor>,
386}
387
388#[derive(Clone, Copy, Debug, PartialEq, Eq)]
389/// `AgenticMemoryRetentionCommandKind` variants.
390pub enum AgenticMemoryRetentionCommandKind {
391    /// `Archive` variant.
392    Archive,
393    /// `Restore` variant.
394    Restore,
395    /// `RequestConsolidation` variant.
396    RequestConsolidation,
397}
398
399#[derive(Clone, Debug, PartialEq, Eq)]
400/// `AgenticMemoryRetentionCommand` data container.
401pub struct AgenticMemoryRetentionCommand {
402    /// `id` field for id.
403    pub id: FactId,
404    /// `record_id` field for record id.
405    pub record_id: FactId,
406    /// `kind` field for kind.
407    pub kind: AgenticMemoryRetentionCommandKind,
408    /// `reason` field for reason.
409    pub reason: Option<String>,
410}
411
412#[derive(Clone, Debug, PartialEq, Eq)]
413/// `AgenticMemoryConsolidationRequest` data container.
414pub struct AgenticMemoryConsolidationRequest {
415    /// `command_id` field for command id.
416    pub command_id: FactId,
417    /// `record_id` field for record id.
418    pub record_id: FactId,
419    /// `fragment_id` field for fragment id.
420    pub fragment_id: FactId,
421    /// `reason` field for reason.
422    pub reason: Option<String>,
423}
424
425#[derive(Clone, Debug, PartialEq, Eq)]
426/// `AgenticMemoryRetentionCursor` data container.
427pub struct AgenticMemoryRetentionCursor {
428    /// `evaluation` field for evaluation.
429    pub evaluation: u64,
430    /// `valid_records` field for valid records.
431    pub valid_records: usize,
432    /// `valid_commands` field for valid commands.
433    pub valid_commands: usize,
434    /// `invalid_commands` field for invalid commands.
435    pub invalid_commands: usize,
436    /// `active_records` field for active records.
437    pub active_records: usize,
438    /// `archived_records` field for archived records.
439    pub archived_records: usize,
440    /// `consolidation_requests` field for consolidation requests.
441    pub consolidation_requests: usize,
442}
443
444#[derive(Clone, Debug, PartialEq)]
445/// `AgenticMemoryRetentionStatus` data container.
446pub struct AgenticMemoryRetentionStatus {
447    /// `state` field for state.
448    pub state: AgenticMemoryStatusState,
449    /// `cursor` field for cursor.
450    pub cursor: AgenticMemoryRetentionCursor,
451}
452
453#[derive(Clone, Debug, PartialEq)]
454/// `AgenticMemoryRetentionSnapshot` data container.
455pub struct AgenticMemoryRetentionSnapshot<T> {
456    /// `active_records` field for active records.
457    pub active_records: Vec<AgenticMemoryRecord<T>>,
458    /// `archived_records` field for archived records.
459    pub archived_records: Vec<AgenticMemoryRecord<T>>,
460    /// `consolidation_requests` field for consolidation requests.
461    pub consolidation_requests: Vec<AgenticMemoryConsolidationRequest>,
462    /// `status` field for status.
463    pub status: AgenticMemoryRetentionStatus,
464    /// `errors` field for errors.
465    pub errors: Vec<AgenticMemoryError>,
466    /// `cursor` field for cursor.
467    pub cursor: AgenticMemoryRetentionCursor,
468}
469
470#[derive(Clone)]
471/// `AgenticMemoryRetentionBundleOptions` data container.
472pub struct AgenticMemoryRetentionBundleOptions<T> {
473    /// `name` field for name.
474    pub name: Option<String>,
475    /// `records` field for records.
476    pub records: Node<Vec<AgenticMemoryRecord<T>>>,
477    /// `commands` field for commands.
478    pub commands: Node<Vec<AgenticMemoryRetentionCommand>>,
479}
480
481impl<T> AgenticMemoryRetentionBundleOptions<T> {
482    /// Creates or computes `new`.
483    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    /// Updates or reads `named`.
495    pub fn named(mut self, name: impl Into<String>) -> Self {
496        self.name = Some(name.into());
497        self
498    }
499}
500
501#[derive(Clone)]
502/// `AgenticMemoryRetentionBundle` data container.
503pub struct AgenticMemoryRetentionBundle<T> {
504    /// `records_input` field for records input.
505    pub records_input: Node<Vec<AgenticMemoryRecord<T>>>,
506    /// `commands_input` field for commands input.
507    pub commands_input: Node<Vec<AgenticMemoryRetentionCommand>>,
508    /// `snapshot` field for snapshot.
509    pub snapshot: Node<AgenticMemoryRetentionSnapshot<T>>,
510    /// `active_records` field for active records.
511    pub active_records: Node<Vec<AgenticMemoryRecord<T>>>,
512    /// `archived_records` field for archived records.
513    pub archived_records: Node<Vec<AgenticMemoryRecord<T>>>,
514    /// `consolidation_requests` field for consolidation requests.
515    pub consolidation_requests: Node<Vec<AgenticMemoryConsolidationRequest>>,
516    /// `status` field for status.
517    pub status: Node<AgenticMemoryRetentionStatus>,
518    /// `errors` field for errors.
519    pub errors: Node<Vec<AgenticMemoryError>>,
520    /// `cursor` field for cursor.
521    pub cursor: Node<AgenticMemoryRetentionCursor>,
522}
523
524#[derive(Clone, Debug, PartialEq)]
525/// `AgenticMemoryConsolidationOutcome` variants.
526pub enum AgenticMemoryConsolidationOutcome<T> {
527    /// `ProposedRecords` variant.
528    ProposedRecords {
529        /// `id` field for id.
530        id: FactId,
531        /// `request_id` field for request id.
532        request_id: FactId,
533        /// `records` field for records.
534        records: Vec<AgenticMemoryRecord<T>>,
535        /// `provenance` field for provenance.
536        provenance: Option<String>,
537    },
538    /// `Failed` variant.
539    Failed {
540        /// `id` field for id.
541        id: FactId,
542        /// `request_id` field for request id.
543        request_id: FactId,
544        /// `message` field for message.
545        message: String,
546        /// `provenance` field for provenance.
547        provenance: Option<String>,
548    },
549}
550
551#[derive(Clone, Debug, PartialEq)]
552/// `AgenticMemoryConsolidationRecordDraft` data container.
553pub struct AgenticMemoryConsolidationRecordDraft<T> {
554    /// `id` field for id.
555    pub id: FactId,
556    /// `request_id` field for request id.
557    pub request_id: FactId,
558    /// `outcome_id` field for outcome id.
559    pub outcome_id: FactId,
560    /// `record` field for record.
561    pub record: AgenticMemoryRecord<T>,
562}
563
564#[derive(Clone, Debug, PartialEq, Eq)]
565/// `AgenticMemoryConsolidationCommandKind` variants.
566pub enum AgenticMemoryConsolidationCommandKind {
567    /// `ProposeRecords` variant.
568    ProposeRecords,
569    /// `MarkFailed` variant.
570    MarkFailed,
571}
572
573#[derive(Clone, Debug, PartialEq, Eq)]
574/// `AgenticMemoryConsolidationCommand` data container.
575pub struct AgenticMemoryConsolidationCommand {
576    /// `id` field for id.
577    pub id: FactId,
578    /// `kind` field for kind.
579    pub kind: AgenticMemoryConsolidationCommandKind,
580    /// `request_id` field for request id.
581    pub request_id: FactId,
582    /// `outcome_id` field for outcome id.
583    pub outcome_id: FactId,
584    /// `draft_ids` field for draft ids.
585    pub draft_ids: Vec<FactId>,
586    /// `message` field for message.
587    pub message: Option<String>,
588}
589
590#[derive(Clone, Debug, PartialEq, Eq)]
591/// `AgenticMemoryConsolidationResultState` variants.
592pub enum AgenticMemoryConsolidationResultState {
593    /// `Proposed` variant.
594    Proposed,
595    /// `Failed` variant.
596    Failed,
597}
598
599#[derive(Clone, Debug, PartialEq, Eq)]
600/// `AgenticMemoryConsolidationResult` data container.
601pub struct AgenticMemoryConsolidationResult {
602    /// `id` field for id.
603    pub id: FactId,
604    /// `request_id` field for request id.
605    pub request_id: FactId,
606    /// `outcome_id` field for outcome id.
607    pub outcome_id: FactId,
608    /// `state` field for state.
609    pub state: AgenticMemoryConsolidationResultState,
610    /// `source_record_ids` field for source record ids.
611    pub source_record_ids: Vec<FactId>,
612    /// `proposed_record_ids` field for proposed record ids.
613    pub proposed_record_ids: Vec<FactId>,
614    /// `message` field for message.
615    pub message: Option<String>,
616    /// `provenance` field for provenance.
617    pub provenance: Option<String>,
618}
619
620#[derive(Clone, Debug, PartialEq, Eq)]
621/// `AgenticMemoryConsolidationCursor` data container.
622pub struct AgenticMemoryConsolidationCursor {
623    /// `evaluation` field for evaluation.
624    pub evaluation: u64,
625    /// `valid_requests` field for valid requests.
626    pub valid_requests: usize,
627    /// `valid_outcomes` field for valid outcomes.
628    pub valid_outcomes: usize,
629    /// `invalid_outcomes` field for invalid outcomes.
630    pub invalid_outcomes: usize,
631    /// `results` field for results.
632    pub results: usize,
633    /// `proposed_record_drafts` field for proposed record drafts.
634    pub proposed_record_drafts: usize,
635}
636
637#[derive(Clone, Debug, PartialEq)]
638/// `AgenticMemoryConsolidationStatus` data container.
639pub struct AgenticMemoryConsolidationStatus {
640    /// `state` field for state.
641    pub state: AgenticMemoryStatusState,
642    /// `cursor` field for cursor.
643    pub cursor: AgenticMemoryConsolidationCursor,
644}
645
646#[derive(Clone, Debug, PartialEq)]
647/// `AgenticMemoryConsolidationSnapshot` data container.
648pub struct AgenticMemoryConsolidationSnapshot<T> {
649    /// `results` field for results.
650    pub results: Vec<AgenticMemoryConsolidationResult>,
651    /// `proposed_record_drafts` field for proposed record drafts.
652    pub proposed_record_drafts: Vec<AgenticMemoryConsolidationRecordDraft<T>>,
653    /// `commands` field for commands.
654    pub commands: Vec<AgenticMemoryConsolidationCommand>,
655    /// `status` field for status.
656    pub status: AgenticMemoryConsolidationStatus,
657    /// `errors` field for errors.
658    pub errors: Vec<AgenticMemoryError>,
659    /// `cursor` field for cursor.
660    pub cursor: AgenticMemoryConsolidationCursor,
661}
662
663#[derive(Clone)]
664/// `AgenticMemoryConsolidationBundleOptions` data container.
665pub struct AgenticMemoryConsolidationBundleOptions<T> {
666    /// `name` field for name.
667    pub name: Option<String>,
668    /// `requests` field for requests.
669    pub requests: Node<Vec<AgenticMemoryConsolidationRequest>>,
670    /// `outcomes` field for outcomes.
671    pub outcomes: Node<Vec<AgenticMemoryConsolidationOutcome<T>>>,
672}
673
674impl<T> AgenticMemoryConsolidationBundleOptions<T> {
675    /// Creates or computes `new`.
676    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    /// Updates or reads `named`.
688    pub fn named(mut self, name: impl Into<String>) -> Self {
689        self.name = Some(name.into());
690        self
691    }
692}
693
694#[derive(Clone)]
695/// `AgenticMemoryConsolidationBundle` data container.
696pub struct AgenticMemoryConsolidationBundle<T> {
697    /// `requests_input` field for requests input.
698    pub requests_input: Node<Vec<AgenticMemoryConsolidationRequest>>,
699    /// `outcomes_input` field for outcomes input.
700    pub outcomes_input: Node<Vec<AgenticMemoryConsolidationOutcome<T>>>,
701    /// `snapshot` field for snapshot.
702    pub snapshot: Node<AgenticMemoryConsolidationSnapshot<T>>,
703    /// `results` field for results.
704    pub results: Node<Vec<AgenticMemoryConsolidationResult>>,
705    /// `proposed_record_drafts` field for proposed record drafts.
706    pub proposed_record_drafts: Node<Vec<AgenticMemoryConsolidationRecordDraft<T>>>,
707    /// `commands` field for commands.
708    pub commands: Node<Vec<AgenticMemoryConsolidationCommand>>,
709    /// `status` field for status.
710    pub status: Node<AgenticMemoryConsolidationStatus>,
711    /// `errors` field for errors.
712    pub errors: Node<Vec<AgenticMemoryError>>,
713    /// `cursor` field for cursor.
714    pub cursor: Node<AgenticMemoryConsolidationCursor>,
715}
716
717#[derive(Clone, Debug, PartialEq, Eq)]
718/// `AgenticMemoryTextProjection` data container.
719pub struct AgenticMemoryTextProjection {
720    /// `fragment_id` field for fragment id.
721    pub fragment_id: FactId,
722    /// `text` field for text.
723    pub text: String,
724}
725
726#[derive(Clone, Debug, PartialEq, Eq)]
727/// `AgenticMemoryContextPackingPolicy` data container.
728pub struct AgenticMemoryContextPackingPolicy {
729    /// `max_chars` field for max chars.
730    pub max_chars: Option<usize>,
731    /// `separator` field for separator.
732    pub separator: String,
733    /// `include_fragment_ids` field for include fragment ids.
734    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)]
748/// `AgenticMemoryContextPackingCursor` data container.
749pub struct AgenticMemoryContextPackingCursor {
750    /// `evaluation` field for evaluation.
751    pub evaluation: u64,
752    /// `context_entries` field for context entries.
753    pub context_entries: usize,
754    /// `text_projection_count` field for text projection count.
755    pub text_projection_count: usize,
756    /// `packed_entries` field for packed entries.
757    pub packed_entries: usize,
758    /// `missing_text` field for missing text.
759    pub missing_text: usize,
760    /// `char_count` field for char count.
761    pub char_count: usize,
762}
763
764#[derive(Clone, Debug, PartialEq, Eq)]
765/// `AgenticMemoryPackedContext` data container.
766pub struct AgenticMemoryPackedContext {
767    /// `text` field for text.
768    pub text: String,
769    /// `fragment_ids` field for fragment ids.
770    pub fragment_ids: Vec<FactId>,
771    /// `truncated` field for truncated.
772    pub truncated: bool,
773    /// `cursor` field for cursor.
774    pub cursor: AgenticMemoryContextPackingCursor,
775}
776
777#[derive(Clone, Debug, PartialEq)]
778/// `AgenticMemoryContextPackingStatus` data container.
779pub struct AgenticMemoryContextPackingStatus {
780    /// `state` field for state.
781    pub state: AgenticMemoryStatusState,
782    /// `cursor` field for cursor.
783    pub cursor: AgenticMemoryContextPackingCursor,
784}
785
786#[derive(Clone, Debug, PartialEq)]
787/// `AgenticMemoryContextPackingSnapshot` data container.
788pub struct AgenticMemoryContextPackingSnapshot {
789    /// `packed_context` field for packed context.
790    pub packed_context: AgenticMemoryPackedContext,
791    /// `status` field for status.
792    pub status: AgenticMemoryContextPackingStatus,
793    /// `errors` field for errors.
794    pub errors: Vec<AgenticMemoryError>,
795    /// `cursor` field for cursor.
796    pub cursor: AgenticMemoryContextPackingCursor,
797}
798
799#[derive(Clone)]
800/// `AgenticMemoryContextPackingBundleOptions` data container.
801pub struct AgenticMemoryContextPackingBundleOptions<T> {
802    /// `name` field for name.
803    pub name: Option<String>,
804    /// `context` field for context.
805    pub context: Node<AgenticMemoryContext<T>>,
806    /// `texts` field for texts.
807    pub texts: Node<Vec<AgenticMemoryTextProjection>>,
808    /// `policy` field for policy.
809    pub policy: Node<AgenticMemoryContextPackingPolicy>,
810}
811
812impl<T> AgenticMemoryContextPackingBundleOptions<T> {
813    /// Creates or computes `new`.
814    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    /// Updates or reads `named`.
828    pub fn named(mut self, name: impl Into<String>) -> Self {
829        self.name = Some(name.into());
830        self
831    }
832}
833
834#[derive(Clone)]
835/// `AgenticMemoryContextPackingBundle` data container.
836pub struct AgenticMemoryContextPackingBundle<T> {
837    /// `context_input` field for context input.
838    pub context_input: Node<AgenticMemoryContext<T>>,
839    /// `texts_input` field for texts input.
840    pub texts_input: Node<Vec<AgenticMemoryTextProjection>>,
841    /// `policy_input` field for policy input.
842    pub policy_input: Node<AgenticMemoryContextPackingPolicy>,
843    /// `snapshot` field for snapshot.
844    pub snapshot: Node<AgenticMemoryContextPackingSnapshot>,
845    /// `packed_context` field for packed context.
846    pub packed_context: Node<AgenticMemoryPackedContext>,
847    /// `status` field for status.
848    pub status: Node<AgenticMemoryContextPackingStatus>,
849    /// `errors` field for errors.
850    pub errors: Node<Vec<AgenticMemoryError>>,
851    /// `cursor` field for cursor.
852    pub cursor: Node<AgenticMemoryContextPackingCursor>,
853}
854
855#[derive(Clone, Debug, PartialEq)]
856/// `AgenticMemoryContextEntry` data container.
857pub struct AgenticMemoryContextEntry<T> {
858    /// `fragment_id` field for fragment id.
859    pub fragment_id: FactId,
860    /// `payload` field for payload.
861    pub payload: T,
862    /// `confidence` field for confidence.
863    pub confidence: f64,
864    /// `tags` field for tags.
865    pub tags: Vec<String>,
866    /// `sources` field for sources.
867    pub sources: Vec<FactId>,
868    /// `fragment` field for fragment.
869    pub fragment: MemoryFragment<T>,
870    /// `metadata` field for metadata.
871    pub metadata: Option<AgenticMemoryRecordMetadata>,
872}
873
874#[derive(Clone, Debug, PartialEq)]
875/// `AgenticMemoryContext` data container.
876pub struct AgenticMemoryContext<T> {
877    /// `state` field for state.
878    pub state: AgenticMemoryStatusState,
879    /// `query` field for query.
880    pub query: MemoryRetrievalQuery,
881    /// `entries` field for entries.
882    pub entries: Vec<AgenticMemoryContextEntry<T>>,
883    /// `cursor` field for cursor.
884    pub cursor: AgenticMemoryCursor,
885    /// `errors` field for errors.
886    pub errors: Vec<AgenticMemoryError>,
887    /// `retrieval_status` field for retrieval status.
888    pub retrieval_status: MemoryRetrievalStatus,
889    /// `retrieval_errors` field for retrieval errors.
890    pub retrieval_errors: Vec<MemoryRetrievalError>,
891    /// `context_ready` field for context ready.
892    pub context_ready: bool,
893}
894
895#[derive(Clone, Debug, PartialEq)]
896/// `AgenticMemoryProjection` data container.
897pub struct AgenticMemoryProjection<T> {
898    /// `records` field for records.
899    pub records: Vec<AgenticMemoryRecord<T>>,
900    /// `fragments` field for fragments.
901    pub fragments: Vec<MemoryFragment<T>>,
902    /// `metadata_by_fragment_id` field for metadata by fragment id.
903    pub metadata_by_fragment_id: BTreeMap<FactId, AgenticMemoryRecordMetadata>,
904    /// `sources` field for sources.
905    pub sources: Vec<AgenticMemorySourceProjection>,
906    /// `errors` field for errors.
907    pub errors: Vec<AgenticMemoryError>,
908    /// `cursor` field for cursor.
909    pub cursor: AgenticMemoryCursor,
910}
911
912#[derive(Clone)]
913/// `AgenticMemoryBundleOptions` data container.
914pub struct AgenticMemoryBundleOptions<T> {
915    /// `name` field for name.
916    pub name: Option<String>,
917    /// `records` field for records.
918    pub records: Node<Vec<AgenticMemoryRecord<T>>>,
919    /// `query` field for query.
920    pub query: Node<MemoryRetrievalQuery>,
921}
922
923impl<T> AgenticMemoryBundleOptions<T> {
924    /// Creates or computes `new`.
925    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    /// Updates or reads `named`.
937    pub fn named(mut self, name: impl Into<String>) -> Self {
938        self.name = Some(name.into());
939        self
940    }
941}
942
943#[derive(Clone)]
944/// `AgenticMemoryBundle` data container.
945pub struct AgenticMemoryBundle<T> {
946    /// `records_input` field for records input.
947    pub records_input: Node<Vec<AgenticMemoryRecord<T>>>,
948    /// `query_input` field for query input.
949    pub query_input: Node<MemoryRetrievalQuery>,
950    /// `projection` field for projection.
951    pub projection: Node<AgenticMemoryProjection<T>>,
952    /// `retrieval` field for retrieval.
953    pub retrieval: MemoryRetrievalBundle<T>,
954    /// `retrieval_snapshot` field for retrieval snapshot.
955    pub retrieval_snapshot: Node<MemoryRetrievalSnapshot<T>>,
956    /// `retrieval_status` field for retrieval status.
957    pub retrieval_status: Node<MemoryRetrievalStatus>,
958    /// `retrieval_errors` field for retrieval errors.
959    pub retrieval_errors: Node<Vec<MemoryRetrievalError>>,
960    /// `fragments` field for fragments.
961    pub fragments: Node<Vec<MemoryFragment<T>>>,
962    /// `sources` field for sources.
963    pub sources: Node<Vec<AgenticMemorySourceProjection>>,
964    /// `indexed` field for indexed.
965    pub indexed: Node<MemoryRetrievalIndex<T>>,
966    /// `ranked` field for ranked.
967    pub ranked: Node<MemoryAnswer<T>>,
968    /// `context` field for context.
969    pub context: Node<AgenticMemoryContext<T>>,
970    /// `status` field for status.
971    pub status: Node<AgenticMemoryStatus>,
972    /// `errors` field for errors.
973    pub errors: Node<Vec<AgenticMemoryError>>,
974    /// `cursor` field for cursor.
975    pub cursor: Node<AgenticMemoryCursor>,
976}
977
978/// Creates or computes `validate_agentic_memory_kind`.
979pub fn validate_agentic_memory_kind(_kind: &AgenticMemoryKind) -> AgenticMemoryFieldValidation {
980    AgenticMemoryFieldValidation {
981        ok: true,
982        errors: Vec::new(),
983    }
984}
985
986/// Creates or computes `validate_agentic_memory_persistence_level`.
987pub fn validate_agentic_memory_persistence_level(
988    _level: &AgenticMemoryPersistenceLevel,
989) -> AgenticMemoryFieldValidation {
990    AgenticMemoryFieldValidation {
991        ok: true,
992        errors: Vec::new(),
993    }
994}
995
996/// Creates or computes `validate_agentic_memory_artifact_kind`.
997pub fn validate_agentic_memory_artifact_kind(
998    _kind: &AgenticMemoryArtifactKind,
999) -> AgenticMemoryFieldValidation {
1000    AgenticMemoryFieldValidation {
1001        ok: true,
1002        errors: Vec::new(),
1003    }
1004}
1005
1006/// Creates or computes `validate_agentic_memory_scope`.
1007pub 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
1039/// Creates or computes `validate_agentic_memory_record`.
1040pub 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
1063/// Compose D164 agentic-memory records into retrieval and context DATA facts.
1064///
1065/// The bundle owns no storage restore/hydration, schedulers, TTL timers, vector
1066/// DB handles, LLM/tool runners, or protocol behavior. It validates the record
1067/// envelope, projects valid records to [`MemoryFragment`] facts, then feeds the
1068/// lower [`memory_retrieval_bundle`] pattern through declared graph deps.
1069pub 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
1253/// Creates or computes `agentic_memory_kg_projection_bundle`.
1254pub 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
1434/// Creates or computes `agentic_memory_retention_bundle`.
1435pub 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
1620/// Creates or computes `agentic_memory_consolidation_bundle`.
1621pub 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
1754/// Creates or computes `agentic_memory_context_packing_bundle`.
1755pub 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}