1use crate::config::{
2 EngineTuningInfo, RecallPlanningProfile, RecallPolicyProfile, RecallScorerKind,
3 RecallScoringProfile,
4};
5use crate::model::{
6 ConflictResolutionKind, ConflictReviewState, EpisodeContinuityState, MemoryHistoricalState,
7 MemoryQualityState, MemoryRecord, MemoryRecordKind, MemoryScope, MemoryTrustLevel,
8};
9use serde::{Deserialize, Serialize};
10use std::collections::{BTreeMap, BTreeSet};
11
12#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
13pub enum RecallTemporalOrder {
14 #[default]
15 Relevance,
16 ChronologicalAsc,
17 ChronologicalDesc,
18}
19
20#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
21pub enum RecallHistoricalMode {
22 #[default]
23 CurrentOnly,
24 IncludeHistorical,
25 HistoricalOnly,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
29#[serde(default)]
30pub struct RecallFilters {
31 pub kinds: Vec<MemoryRecordKind>,
32 pub required_labels: Vec<String>,
33 pub source: Option<String>,
34 pub from_unix_ms: Option<u64>,
35 pub to_unix_ms: Option<u64>,
36 pub min_importance_score: Option<f32>,
37 pub trust_levels: Vec<MemoryTrustLevel>,
38 pub states: Vec<MemoryQualityState>,
39 pub include_archived: bool,
40 pub episode_id: Option<String>,
41 pub continuity_states: Vec<EpisodeContinuityState>,
42 pub unresolved_only: bool,
43 pub temporal_order: RecallTemporalOrder,
44 pub historical_mode: RecallHistoricalMode,
45 pub lineage_record_id: Option<String>,
46 pub before_record_id: Option<String>,
47 pub after_record_id: Option<String>,
48 pub boundary_labels: Vec<String>,
49 pub recurrence_key: Option<String>,
50 pub conflict_states: Vec<ConflictReviewState>,
51 pub resolution_kinds: Vec<ConflictResolutionKind>,
52 pub unresolved_conflicts_only: bool,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
56pub struct RecallQuery {
57 pub scope: MemoryScope,
58 pub query_text: String,
59 pub max_items: usize,
60 pub token_budget: Option<usize>,
61 pub filters: RecallFilters,
62 pub include_explanation: bool,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
66pub struct TimeTravelRecallRequest {
67 pub query: RecallQuery,
68 pub as_of_unix_ms: u64,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
72pub struct RecallScoreBreakdown {
73 pub lexical: f32,
74 pub semantic: f32,
75 pub graph: f32,
76 pub temporal: f32,
77 pub metadata: f32,
78 pub episodic: f32,
79 pub salience: f32,
80 pub curation: f32,
81 pub policy: f32,
82 pub total: f32,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
86pub struct RecallExplanation {
87 pub selected_channels: Vec<String>,
88 pub policy_notes: Vec<String>,
89 pub trace_id: Option<String>,
90 pub planning_trace: Option<RecallPlanningTrace>,
91 pub planning_profile: Option<RecallPlanningProfile>,
92 pub policy_profile: Option<RecallPolicyProfile>,
93 pub scorer_kind: Option<RecallScorerKind>,
94 pub scoring_profile: Option<RecallScoringProfile>,
95}
96
97#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Default)]
98pub enum RecallPlannerStage {
99 #[default]
100 CandidateGeneration,
101 GraphExpansion,
102 Selection,
103}
104
105#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
106pub enum RecallCandidateSource {
107 Lexical,
108 Semantic,
109 Metadata,
110 Episode,
111 Graph,
112 Temporal,
113 Provenance,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
117pub struct RecallPlanningTrace {
118 pub trace_id: String,
119 pub token_budget_applied: bool,
120 pub candidates: Vec<RecallTraceCandidate>,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
124pub struct RecallTraceCandidate {
125 pub record_id: String,
126 pub kind: MemoryRecordKind,
127 pub selected: bool,
128 pub planner_stage: RecallPlannerStage,
129 pub candidate_sources: Vec<RecallCandidateSource>,
130 pub relation_reasons: Vec<String>,
131 pub selection_rank: Option<u32>,
132 pub matched_terms: Vec<String>,
133 pub selected_channels: Vec<String>,
134 pub filter_reasons: Vec<String>,
135 pub decision_reason: String,
136 pub breakdown: RecallScoreBreakdown,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
140pub struct RecallHit {
141 pub record: MemoryRecord,
142 pub breakdown: RecallScoreBreakdown,
143 pub explanation: Option<RecallExplanation>,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
147pub struct RecallResult {
148 pub hits: Vec<RecallHit>,
149 pub total_candidates_examined: usize,
150 pub explanation: Option<RecallExplanation>,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
154pub struct CompactionRequest {
155 pub tenant_id: String,
156 pub namespace: Option<String>,
157 pub dry_run: bool,
158 pub reason: String,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
162pub struct CompactionReport {
163 pub deduplicated_records: u64,
164 pub archived_records: u64,
165 pub summarized_clusters: u64,
166 pub pruned_graph_edges: u64,
167 pub superseded_records: u64,
168 pub lineage_links_created: u64,
169 pub dry_run: bool,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
173#[serde(default)]
174pub struct SynthesisRequest {
175 pub tenant_id: String,
176 pub namespace: Option<String>,
177 pub actor_id: Option<String>,
178 pub conversation_id: Option<String>,
179 pub session_id: Option<String>,
180 pub from_unix_ms: Option<u64>,
181 pub to_unix_ms: Option<u64>,
182 pub min_source_records: usize,
183 pub max_source_records: usize,
184 pub max_proposals: usize,
185 pub dry_run: bool,
186 pub reason: String,
187}
188
189impl Default for SynthesisRequest {
190 fn default() -> Self {
191 Self {
192 tenant_id: String::new(),
193 namespace: None,
194 actor_id: None,
195 conversation_id: None,
196 session_id: None,
197 from_unix_ms: None,
198 to_unix_ms: None,
199 min_source_records: 2,
200 max_source_records: 12,
201 max_proposals: 16,
202 dry_run: true,
203 reason: "memory synthesis proposal".to_string(),
204 }
205 }
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
209pub struct SynthesisProposal {
210 pub proposed_record: MemoryRecord,
211 pub source_record_ids: Vec<String>,
212 pub confidence: f32,
213 pub rationale: String,
214 pub metadata: BTreeMap<String, String>,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
218pub struct SynthesisReport {
219 pub dry_run: bool,
220 pub scanned_records: u64,
221 pub eligible_records: u64,
222 pub proposed_records: u64,
223 pub persisted_records: u64,
224 pub lineage_links_created: u64,
225 pub proposals: Vec<SynthesisProposal>,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
229pub struct SnapshotManifest {
230 pub snapshot_id: String,
231 pub created_at_unix_ms: u64,
232 pub namespaces: Vec<String>,
233 pub record_count: u64,
234 pub storage_bytes: u64,
235 pub engine: EngineTuningInfo,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
239pub struct StoreStatsRequest {
240 pub tenant_id: Option<String>,
241 pub namespace: Option<String>,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
245pub struct NamespaceStats {
246 pub tenant_id: String,
247 pub namespace: String,
248 pub active_records: u64,
249 pub archived_records: u64,
250 pub deleted_records: u64,
251 pub suppressed_records: u64,
252 pub pinned_records: u64,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
256pub struct MaintenanceStats {
257 pub duplicate_candidate_groups: u64,
258 pub duplicate_candidate_records: u64,
259 pub tombstoned_records: u64,
260 pub expired_records: u64,
261 pub stale_idempotency_keys: u64,
262 pub historical_records: u64,
263 pub superseded_records: u64,
264 pub lineage_links: u64,
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
268pub struct StoreStatsReport {
269 pub generated_at_unix_ms: u64,
270 pub total_records: u64,
271 pub storage_bytes: u64,
272 pub namespaces: Vec<NamespaceStats>,
273 pub maintenance: MaintenanceStats,
274 pub engine: EngineTuningInfo,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
278#[serde(default)]
279pub struct GraphInspectionRequest {
280 pub tenant_id: Option<String>,
281 pub namespace: Option<String>,
282 pub actor_id: Option<String>,
283 pub conversation_id: Option<String>,
284 pub session_id: Option<String>,
285 pub include_archived: bool,
286 pub include_suppressed: bool,
287 pub include_deleted: bool,
288 pub max_nodes: Option<usize>,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
292pub struct GraphInspectionNode {
293 pub record_id: String,
294 pub tenant_id: String,
295 pub namespace: String,
296 pub actor_id: String,
297 pub kind: MemoryRecordKind,
298 pub summary: Option<String>,
299 pub quality_state: MemoryQualityState,
300 pub historical_state: MemoryHistoricalState,
301 pub episode_id: Option<String>,
302 pub continuity_state: Option<EpisodeContinuityState>,
303 pub conflict_state: Option<ConflictReviewState>,
304 pub importance_per_mille: u16,
305 pub updated_at_unix_ms: u64,
306}
307
308#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
309pub enum GraphInspectionEdgeKind {
310 EpisodeMembership,
311 ChronologyPrevious,
312 ChronologyNext,
313 Causal,
314 Related,
315 Lineage,
316 Conflict,
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
320pub struct GraphInspectionEdge {
321 pub source_id: String,
322 pub target_id: String,
323 pub kind: GraphInspectionEdgeKind,
324 pub details: Vec<String>,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
328pub struct GraphInspectionReport {
329 pub generated_at_unix_ms: u64,
330 pub total_records_scanned: u64,
331 pub nodes: Vec<GraphInspectionNode>,
332 pub edges: Vec<GraphInspectionEdge>,
333 pub truncated: bool,
334}
335
336pub fn build_graph_inspection_report(
337 records: &[MemoryRecord],
338 request: &GraphInspectionRequest,
339 generated_at_unix_ms: u64,
340) -> GraphInspectionReport {
341 let mut nodes = Vec::new();
342 let mut included_ids = BTreeSet::new();
343 let mut truncated = false;
344
345 for record in records {
346 if !graph_inspection_includes_record(record, request) {
347 continue;
348 }
349 if request.max_nodes.is_some_and(|limit| nodes.len() >= limit) {
350 truncated = true;
351 break;
352 }
353 included_ids.insert(record.id.clone());
354 nodes.push(graph_inspection_node(record));
355 }
356
357 let mut edge_keys = BTreeSet::new();
358 let mut edges = Vec::new();
359 for record in records {
360 if !included_ids.contains(&record.id) {
361 continue;
362 }
363 graph_inspection_edges(record, &included_ids, &mut edge_keys, &mut edges);
364 }
365
366 GraphInspectionReport {
367 generated_at_unix_ms,
368 total_records_scanned: records.len() as u64,
369 nodes,
370 edges,
371 truncated,
372 }
373}
374
375fn graph_inspection_includes_record(
376 record: &MemoryRecord,
377 request: &GraphInspectionRequest,
378) -> bool {
379 if request
380 .tenant_id
381 .as_ref()
382 .is_some_and(|expected| &record.scope.tenant_id != expected)
383 || request
384 .namespace
385 .as_ref()
386 .is_some_and(|expected| &record.scope.namespace != expected)
387 || request
388 .actor_id
389 .as_ref()
390 .is_some_and(|expected| &record.scope.actor_id != expected)
391 || request
392 .conversation_id
393 .as_ref()
394 .is_some_and(|expected| record.scope.conversation_id.as_ref() != Some(expected))
395 || request
396 .session_id
397 .as_ref()
398 .is_some_and(|expected| record.scope.session_id.as_ref() != Some(expected))
399 {
400 return false;
401 }
402
403 match record.quality_state {
404 MemoryQualityState::Archived => request.include_archived,
405 MemoryQualityState::Suppressed => request.include_suppressed,
406 MemoryQualityState::Deleted => request.include_deleted,
407 _ => true,
408 }
409}
410
411fn graph_inspection_node(record: &MemoryRecord) -> GraphInspectionNode {
412 GraphInspectionNode {
413 record_id: record.id.clone(),
414 tenant_id: record.scope.tenant_id.clone(),
415 namespace: record.scope.namespace.clone(),
416 actor_id: record.scope.actor_id.clone(),
417 kind: record.kind,
418 summary: record.summary.clone(),
419 quality_state: record.quality_state,
420 historical_state: record.historical_state,
421 episode_id: record
422 .episode
423 .as_ref()
424 .map(|episode| episode.episode_id.clone()),
425 continuity_state: record
426 .episode
427 .as_ref()
428 .map(|episode| episode.continuity_state),
429 conflict_state: record.conflict.as_ref().map(|conflict| conflict.state),
430 importance_per_mille: (record.importance_score.clamp(0.0, 1.0) * 1000.0).round() as u16,
431 updated_at_unix_ms: record.updated_at_unix_ms,
432 }
433}
434
435fn push_graph_inspection_edge(
436 edge_keys: &mut BTreeSet<(String, String, GraphInspectionEdgeKind)>,
437 edges: &mut Vec<GraphInspectionEdge>,
438 source_record_id: &str,
439 target_record_id: &str,
440 kind: GraphInspectionEdgeKind,
441 details: Vec<String>,
442) {
443 if source_record_id == target_record_id {
444 return;
445 }
446 let key = (
447 source_record_id.to_string(),
448 target_record_id.to_string(),
449 kind,
450 );
451 if edge_keys.insert(key) {
452 edges.push(GraphInspectionEdge {
453 source_id: source_record_id.to_string(),
454 target_id: target_record_id.to_string(),
455 kind,
456 details,
457 });
458 }
459}
460
461fn graph_inspection_edges(
462 record: &MemoryRecord,
463 included_ids: &BTreeSet<String>,
464 edge_keys: &mut BTreeSet<(String, String, GraphInspectionEdgeKind)>,
465 edges: &mut Vec<GraphInspectionEdge>,
466) {
467 if let Some(episode) = &record.episode {
468 if !episode.episode_id.is_empty() {
469 push_graph_inspection_edge(
470 edge_keys,
471 edges,
472 &record.id,
473 &format!("episode:{}", episode.episode_id),
474 GraphInspectionEdgeKind::EpisodeMembership,
475 vec![format!("continuity_state={:?}", episode.continuity_state)],
476 );
477 }
478 if let Some(previous) = &episode.previous_record_id
479 && included_ids.contains(previous)
480 {
481 push_graph_inspection_edge(
482 edge_keys,
483 edges,
484 &record.id,
485 previous,
486 GraphInspectionEdgeKind::ChronologyPrevious,
487 Vec::new(),
488 );
489 }
490 if let Some(next) = &episode.next_record_id
491 && included_ids.contains(next)
492 {
493 push_graph_inspection_edge(
494 edge_keys,
495 edges,
496 &record.id,
497 next,
498 GraphInspectionEdgeKind::ChronologyNext,
499 Vec::new(),
500 );
501 }
502 for causal_id in &episode.causal_record_ids {
503 if included_ids.contains(causal_id) {
504 push_graph_inspection_edge(
505 edge_keys,
506 edges,
507 &record.id,
508 causal_id,
509 GraphInspectionEdgeKind::Causal,
510 Vec::new(),
511 );
512 }
513 }
514 for related_id in &episode.related_record_ids {
515 if included_ids.contains(related_id) {
516 push_graph_inspection_edge(
517 edge_keys,
518 edges,
519 &record.id,
520 related_id,
521 GraphInspectionEdgeKind::Related,
522 Vec::new(),
523 );
524 }
525 }
526 }
527
528 for link in &record.lineage {
529 if included_ids.contains(&link.record_id) {
530 push_graph_inspection_edge(
531 edge_keys,
532 edges,
533 &record.id,
534 &link.record_id,
535 GraphInspectionEdgeKind::Lineage,
536 vec![
537 format!("relation={:?}", link.relation),
538 format!(
539 "confidence_per_mille={}",
540 (link.confidence.clamp(0.0, 1.0) * 1000.0).round() as u16
541 ),
542 ],
543 );
544 }
545 }
546
547 if let Some(conflict) = &record.conflict {
548 for conflicting_id in &conflict.conflicting_record_ids {
549 if included_ids.contains(conflicting_id) {
550 push_graph_inspection_edge(
551 edge_keys,
552 edges,
553 &record.id,
554 conflicting_id,
555 GraphInspectionEdgeKind::Conflict,
556 vec![
557 format!("state={:?}", conflict.state),
558 format!("resolution={:?}", conflict.resolution),
559 ],
560 );
561 }
562 }
563 }
564}
565
566#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
567pub struct IntegrityCheckRequest {
568 pub tenant_id: Option<String>,
569 pub namespace: Option<String>,
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
573pub struct IntegrityCheckReport {
574 pub generated_at_unix_ms: u64,
575 pub healthy: bool,
576 pub scanned_records: u64,
577 pub scanned_idempotency_keys: u64,
578 pub stale_idempotency_keys: u64,
579 pub missing_idempotency_keys: u64,
580 pub duplicate_active_records: u64,
581}
582
583#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
584pub struct RepairRequest {
585 pub tenant_id: Option<String>,
586 pub namespace: Option<String>,
587 pub dry_run: bool,
588 pub reason: String,
589 pub remove_stale_idempotency_keys: bool,
590 pub rebuild_missing_idempotency_keys: bool,
591}
592
593#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
594pub struct RepairReport {
595 pub dry_run: bool,
596 pub scanned_records: u64,
597 pub scanned_idempotency_keys: u64,
598 pub removed_stale_idempotency_keys: u64,
599 pub rebuilt_missing_idempotency_keys: u64,
600 pub healthy_after: bool,
601}
602
603#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
604#[serde(default)]
605pub struct MaintenanceRunRequest {
606 pub tenant_id: Option<String>,
607 pub namespace: Option<String>,
608 pub dry_run: bool,
609 pub reason: String,
610 pub run_integrity_check: bool,
611 pub run_repair: bool,
612 pub run_compaction: bool,
613 pub run_synthesis: bool,
614 pub remove_stale_idempotency_keys: bool,
615 pub rebuild_missing_idempotency_keys: bool,
616}
617
618impl Default for MaintenanceRunRequest {
619 fn default() -> Self {
620 Self {
621 tenant_id: None,
622 namespace: None,
623 dry_run: true,
624 reason: "manual maintenance run".to_string(),
625 run_integrity_check: true,
626 run_repair: true,
627 run_compaction: true,
628 run_synthesis: false,
629 remove_stale_idempotency_keys: true,
630 rebuild_missing_idempotency_keys: true,
631 }
632 }
633}
634
635#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
636pub struct MaintenanceRunReport {
637 pub dry_run: bool,
638 pub integrity_before: Option<IntegrityCheckReport>,
639 pub repair: Option<RepairReport>,
640 pub compaction: Option<CompactionReport>,
641 pub synthesis: Option<SynthesisReport>,
642 pub integrity_after: Option<IntegrityCheckReport>,
643}
644
645#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
646#[serde(default)]
647pub struct SnapshotShipRequest {
648 pub target_url: String,
649 pub bearer_token: Option<String>,
650 pub tenant_id: Option<String>,
651 pub namespace: Option<String>,
652 pub include_archived: bool,
653 pub mode: ImportMode,
654 pub dry_run: bool,
655}
656
657impl Default for SnapshotShipRequest {
658 fn default() -> Self {
659 Self {
660 target_url: String::new(),
661 bearer_token: None,
662 tenant_id: None,
663 namespace: None,
664 include_archived: false,
665 mode: ImportMode::Validate,
666 dry_run: true,
667 }
668 }
669}
670
671#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
672pub struct SnapshotShipReport {
673 pub target_url: String,
674 pub exported_records: u64,
675 pub imported_records: u64,
676 pub skipped_records: u64,
677 pub dry_run: bool,
678 pub compatible_package: bool,
679 pub remote_status: u16,
680 pub remote_snapshot_id: Option<String>,
681}
682
683#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
684pub enum TraceOperationKind {
685 Upsert,
686 BatchUpsert,
687 Recall,
688 Snapshot,
689 Stats,
690 IntegrityCheck,
691 Repair,
692 Compact,
693 Delete,
694 Archive,
695 Suppress,
696 Recover,
697 Export,
698 Import,
699 MaintenanceRun,
700 SnapshotShip,
701 Changefeed,
702 Synthesis,
703}
704
705#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
706pub enum ChangefeedEventKind {
707 Upserted,
708 Deleted,
709 Archived,
710 Suppressed,
711 Recovered,
712 Compacted,
713 Imported,
714 RetentionArchived,
715 RetentionDeleted,
716}
717
718#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
719#[serde(default)]
720pub struct ChangefeedRequest {
721 pub tenant_id: Option<String>,
722 pub namespace: Option<String>,
723 pub after_sequence: Option<u64>,
724 pub limit: Option<usize>,
725}
726
727#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
728pub struct ChangefeedEvent {
729 pub sequence: u64,
730 pub event_id: String,
731 pub kind: ChangefeedEventKind,
732 pub tenant_id: String,
733 pub namespace: String,
734 pub record_id: Option<String>,
735 pub occurred_at_unix_ms: u64,
736 pub summary: Option<String>,
737 pub record: Option<MemoryRecord>,
738}
739
740#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
741pub struct ChangefeedReport {
742 pub events: Vec<ChangefeedEvent>,
743 pub last_sequence: Option<u64>,
744 pub truncated: bool,
745}
746
747#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
748pub enum TraceStatus {
749 Ok,
750 Rejected,
751 Error,
752}
753
754#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
755pub struct OperationTraceSummary {
756 pub record_id: Option<String>,
757 pub request_count: Option<u32>,
758 pub query_text: Option<String>,
759 pub max_items: Option<u32>,
760 pub token_budget: Option<u32>,
761 pub dry_run: Option<bool>,
762}
763
764#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
765pub struct OperationTrace {
766 pub trace_id: String,
767 pub correlation_id: String,
768 pub operation: TraceOperationKind,
769 pub transport: String,
770 pub backend: Option<String>,
771 pub admission_class: Option<String>,
772 pub tenant_id: Option<String>,
773 pub namespace: Option<String>,
774 pub principal: Option<String>,
775 pub store_span_id: Option<String>,
776 pub planning_trace_id: Option<String>,
777 pub started_at_unix_ms: u64,
778 pub completed_at_unix_ms: u64,
779 pub latency_ms: u64,
780 pub status: TraceStatus,
781 pub status_message: Option<String>,
782 pub summary: OperationTraceSummary,
783 pub recall_explanation: Option<RecallExplanation>,
784}
785
786#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
787pub struct TraceListRequest {
788 pub tenant_id: Option<String>,
789 pub namespace: Option<String>,
790 pub operation: Option<TraceOperationKind>,
791 pub status: Option<TraceStatus>,
792 pub before_started_at_unix_ms: Option<u64>,
793 pub limit: Option<usize>,
794}
795
796#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
797pub struct PortableRecord {
798 pub record: MemoryRecord,
799 pub idempotency_key: Option<String>,
800}
801
802#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
803pub struct ExportRequest {
804 pub tenant_id: Option<String>,
805 pub namespace: Option<String>,
806 pub include_archived: bool,
807}
808
809#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
810pub struct PortableStorePackage {
811 pub package_version: u32,
812 pub exported_at_unix_ms: u64,
813 pub manifest: SnapshotManifest,
814 pub records: Vec<PortableRecord>,
815}
816
817#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
818pub enum ImportMode {
819 Validate,
820 Merge,
821 Replace,
822}
823
824#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
825pub struct ImportRequest {
826 pub package: PortableStorePackage,
827 pub mode: ImportMode,
828 pub dry_run: bool,
829}
830
831#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
832pub struct ImportFailure {
833 pub record_id: Option<String>,
834 pub reason: String,
835}
836
837#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
838pub struct ImportReport {
839 pub mode: ImportMode,
840 pub dry_run: bool,
841 pub applied: bool,
842 pub compatible_package: bool,
843 pub package_version: u32,
844 pub validated_records: u64,
845 pub imported_records: u64,
846 pub skipped_records: u64,
847 pub replaced_existing: bool,
848 pub snapshot_id: String,
849 pub failed_records: Vec<ImportFailure>,
850}
851
852#[cfg(test)]
853mod tests {
854 use super::{GraphInspectionEdgeKind, GraphInspectionRequest, build_graph_inspection_report};
855 use crate::model::{
856 ConflictAnnotation, ConflictReviewState, EpisodeContext, EpisodeContinuityState,
857 LineageLink, LineageRelationKind, MemoryHistoricalState, MemoryQualityState, MemoryRecord,
858 MemoryRecordKind, MemoryScope, MemoryTrustLevel,
859 };
860 use std::collections::BTreeMap;
861
862 fn scope() -> MemoryScope {
863 MemoryScope {
864 tenant_id: "tenant-a".to_string(),
865 namespace: "ops".to_string(),
866 actor_id: "ava".to_string(),
867 conversation_id: Some("thread-1".to_string()),
868 session_id: Some("session-1".to_string()),
869 source: "test".to_string(),
870 labels: Vec::new(),
871 trust_level: MemoryTrustLevel::Verified,
872 }
873 }
874
875 fn record(id: &str) -> MemoryRecord {
876 MemoryRecord {
877 id: id.to_string(),
878 scope: scope(),
879 kind: MemoryRecordKind::Episodic,
880 content: format!("content {id}"),
881 summary: Some(format!("summary {id}")),
882 source_id: None,
883 metadata: BTreeMap::new(),
884 quality_state: MemoryQualityState::Active,
885 created_at_unix_ms: 1,
886 updated_at_unix_ms: 2,
887 expires_at_unix_ms: None,
888 importance_score: 0.5,
889 artifact: None,
890 episode: None,
891 historical_state: MemoryHistoricalState::Current,
892 lineage: Vec::new(),
893 conflict: None,
894 }
895 }
896
897 #[test]
898 fn graph_inspection_builds_typed_edges_and_filters_hidden_records() {
899 let mut seed = record("seed");
900 seed.episode = Some(EpisodeContext {
901 episode_id: "incident".to_string(),
902 continuity_state: EpisodeContinuityState::Open,
903 next_record_id: Some("next".to_string()),
904 related_record_ids: vec!["next".to_string()],
905 ..Default::default()
906 });
907
908 let mut next = record("next");
909 next.episode = Some(EpisodeContext {
910 episode_id: "incident".to_string(),
911 continuity_state: EpisodeContinuityState::Open,
912 previous_record_id: Some("seed".to_string()),
913 causal_record_ids: vec!["seed".to_string()],
914 ..Default::default()
915 });
916 next.lineage = vec![LineageLink {
917 record_id: "seed".to_string(),
918 relation: LineageRelationKind::DerivedFrom,
919 confidence: 0.8,
920 }];
921 next.conflict = Some(ConflictAnnotation {
922 state: ConflictReviewState::PotentialConflict,
923 conflicting_record_ids: vec!["seed".to_string()],
924 ..Default::default()
925 });
926
927 let mut hidden = record("hidden");
928 hidden.quality_state = MemoryQualityState::Suppressed;
929
930 let report = build_graph_inspection_report(
931 &[seed, next, hidden],
932 &GraphInspectionRequest {
933 tenant_id: Some("tenant-a".to_string()),
934 namespace: Some("ops".to_string()),
935 ..Default::default()
936 },
937 42,
938 );
939
940 assert_eq!(report.generated_at_unix_ms, 42);
941 assert_eq!(report.nodes.len(), 2);
942 assert!(!report.nodes.iter().any(|node| node.record_id == "hidden"));
943 assert!(report.edges.iter().any(|edge| matches!(
944 edge.kind,
945 GraphInspectionEdgeKind::ChronologyNext | GraphInspectionEdgeKind::ChronologyPrevious
946 )));
947 assert!(
948 report
949 .edges
950 .iter()
951 .any(|edge| edge.kind == GraphInspectionEdgeKind::Causal)
952 );
953 assert!(
954 report
955 .edges
956 .iter()
957 .any(|edge| edge.kind == GraphInspectionEdgeKind::Related)
958 );
959 assert!(
960 report
961 .edges
962 .iter()
963 .any(|edge| edge.kind == GraphInspectionEdgeKind::Lineage)
964 );
965 assert!(
966 report
967 .edges
968 .iter()
969 .any(|edge| edge.kind == GraphInspectionEdgeKind::Conflict)
970 );
971 }
972}