1#![allow(deprecated)]
2
3use crate::error::MemoryError;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use stack_ids::{
7 ClaimId, ClaimVersionId, EntityId, EnvelopeId, EpisodeId, RelationVersionId, ScopeKey,
8};
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[serde(transparent)]
21pub struct CompatTraceId(pub String);
22
23#[deprecated(since = "0.5.0", note = "Use stack_ids::TraceCtx instead")]
24pub type TraceId = CompatTraceId;
25
26impl CompatTraceId {
27 pub fn new(value: impl Into<String>) -> Self {
29 Self(value.into())
30 }
31
32 pub fn as_str(&self) -> &str {
34 &self.0
35 }
36}
37
38impl std::fmt::Display for CompatTraceId {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 f.write_str(&self.0)
41 }
42}
43
44impl From<String> for CompatTraceId {
45 fn from(value: String) -> Self {
46 Self(value)
47 }
48}
49
50impl From<&str> for CompatTraceId {
51 fn from(value: &str) -> Self {
52 Self(value.to_string())
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "lowercase")]
59pub enum Role {
60 System,
62 User,
64 Assistant,
66 Tool,
68}
69
70impl Role {
71 pub fn as_str(&self) -> &'static str {
73 match self {
74 Role::System => "system",
75 Role::User => "user",
76 Role::Assistant => "assistant",
77 Role::Tool => "tool",
78 }
79 }
80
81 pub fn from_str_value(s: &str) -> Option<Self> {
83 match s {
84 "system" => Some(Role::System),
85 "user" => Some(Role::User),
86 "assistant" => Some(Role::Assistant),
87 "tool" => Some(Role::Tool),
88 _ => None,
89 }
90 }
91}
92
93impl std::fmt::Display for Role {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 f.write_str(self.as_str())
96 }
97}
98
99impl std::str::FromStr for Role {
100 type Err = MemoryError;
101
102 fn from_str(s: &str) -> Result<Self, Self::Err> {
103 Self::from_str_value(s).ok_or_else(|| MemoryError::Other(format!("Unknown role: '{}'", s)))
104 }
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(rename_all = "snake_case")]
110pub enum SearchSourceType {
111 Facts,
113 Chunks,
115 Messages,
117 Episodes,
119}
120
121#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
123#[serde(rename_all = "snake_case")]
124pub enum ReplayMode {
125 #[default]
127 NoReplay,
128 StoreInputs,
130}
131
132#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
134#[serde(rename_all = "snake_case")]
135pub enum ReceiptMode {
136 #[default]
138 Disabled,
139 ExplainOnly,
141 ReturnReceipt,
143}
144
145#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
147#[serde(rename_all = "snake_case")]
148pub enum ExactnessProfile {
149 #[default]
151 Default,
152 PreferExact,
154 AllowApproximate,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct SearchContext {
161 pub evaluation_time: DateTime<Utc>,
163 pub receipt_mode: ReceiptMode,
165 #[serde(default)]
167 pub replay_mode: ReplayMode,
168 pub exactness_profile: ExactnessProfile,
170 pub request_id: Option<String>,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub trace_id: Option<String>,
175 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub attempt_family_id: Option<String>,
178 #[serde(default, skip_serializing_if = "Option::is_none")]
180 pub attempt_id: Option<String>,
181 #[serde(default, skip_serializing_if = "Option::is_none")]
183 pub replay_of: Option<String>,
184 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub query_text_digest: Option<String>,
187 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub query_input_digest: Option<String>,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub filter_digest: Option<String>,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub redaction_state: Option<String>,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub budget_id: Option<String>,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub deadline_at: Option<DateTime<Utc>>,
202}
203
204impl SearchContext {
205 pub fn default_now() -> Self {
207 Self {
208 evaluation_time: Utc::now(),
209 receipt_mode: ReceiptMode::Disabled,
210 replay_mode: ReplayMode::NoReplay,
211 exactness_profile: ExactnessProfile::Default,
212 request_id: None,
213 trace_id: None,
214 attempt_family_id: None,
215 attempt_id: None,
216 replay_of: None,
217 query_text_digest: None,
218 query_input_digest: None,
219 filter_digest: None,
220 redaction_state: None,
221 budget_id: None,
222 deadline_at: None,
223 }
224 }
225
226 pub fn at(evaluation_time: DateTime<Utc>) -> Self {
228 Self {
229 evaluation_time,
230 ..Self::default_now()
231 }
232 }
233
234 pub fn receipts_enabled(&self) -> bool {
236 self.receipt_mode != ReceiptMode::Disabled
237 }
238}
239
240impl Default for SearchContext {
241 fn default() -> Self {
242 Self::default_now()
243 }
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct VectorSearchReceiptV1 {
249 #[serde(default = "default_vector_search_receipt_schema")]
251 pub schema_version: String,
252 #[serde(default, skip_serializing_if = "Option::is_none")]
254 pub receipt_digest: Option<String>,
255 pub receipt_id: String,
257 pub evaluation_time: DateTime<Utc>,
259 #[serde(default, skip_serializing_if = "Option::is_none")]
261 pub trace_id: Option<String>,
262 #[serde(default, skip_serializing_if = "Option::is_none")]
264 pub attempt_family_id: Option<String>,
265 #[serde(default, skip_serializing_if = "Option::is_none")]
267 pub attempt_id: Option<String>,
268 #[serde(default, skip_serializing_if = "Option::is_none")]
270 pub replay_of: Option<String>,
271 pub query_embedding_digest: Option<String>,
273 #[serde(default, skip_serializing_if = "Option::is_none")]
275 pub query_text_digest: Option<String>,
276 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub query_input_digest: Option<String>,
279 #[serde(default, skip_serializing_if = "Option::is_none")]
281 pub filter_digest: Option<String>,
282 #[serde(default, skip_serializing_if = "Option::is_none")]
284 pub redaction_state: Option<String>,
285 #[serde(default, skip_serializing_if = "Option::is_none")]
287 pub budget_id: Option<String>,
288 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub deadline_at: Option<DateTime<Utc>>,
291 pub search_profile: String,
293 pub candidate_backend: String,
295 pub codec_family: Option<String>,
297 pub codec_profile_digest: Option<String>,
299 #[serde(default, skip_serializing_if = "Option::is_none")]
301 pub artifact_profile_digest: Option<String>,
302 #[serde(default, skip_serializing_if = "Option::is_none")]
304 pub artifact_count: Option<usize>,
305 #[serde(default, skip_serializing_if = "Option::is_none")]
307 pub artifact_corruption_count: Option<usize>,
308 #[serde(default, skip_serializing_if = "Option::is_none")]
310 pub artifact_missing_count: Option<usize>,
311 #[serde(default, skip_serializing_if = "Option::is_none")]
313 pub vector_artifact_manifest_digest: Option<String>,
314 #[serde(default, skip_serializing_if = "Option::is_none")]
316 pub artifact_generation_id: Option<String>,
317 #[serde(default, skip_serializing_if = "Option::is_none")]
319 pub approximate_scanned_count: Option<usize>,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
322 pub approximate_returned_count: Option<usize>,
323 #[serde(default, skip_serializing_if = "Option::is_none")]
325 pub raw_rows_loaded_count: Option<usize>,
326 #[serde(default, skip_serializing_if = "Option::is_none")]
328 pub filter_strategy: Option<String>,
329 #[serde(default, skip_serializing_if = "Option::is_none")]
331 pub vector_artifact_count: Option<usize>,
332 #[serde(default, skip_serializing_if = "Option::is_none")]
334 pub vector_artifact_missing_count: Option<usize>,
335 #[serde(default, skip_serializing_if = "Option::is_none")]
337 pub vector_artifact_stale_count: Option<usize>,
338 #[serde(default, skip_serializing_if = "Option::is_none")]
340 pub exact_rerank_count: Option<usize>,
341 #[serde(default, skip_serializing_if = "Option::is_none")]
343 pub approximate_candidate_count: Option<usize>,
344 #[serde(default, skip_serializing_if = "Option::is_none")]
346 pub fallback_reason: Option<String>,
347 #[serde(default, skip_serializing_if = "Option::is_none")]
349 pub derived_candidate: Option<DerivedCandidateReceiptV1>,
350 pub approximate: bool,
352 pub requested_candidates: usize,
354 pub returned_candidates: usize,
356 pub post_filter_candidates: usize,
358 #[serde(default)]
360 pub sparse_enabled: bool,
361 #[serde(default, skip_serializing_if = "Option::is_none")]
363 pub sparse_weight: Option<f64>,
364 #[serde(default, skip_serializing_if = "Option::is_none")]
366 pub sparse_query_nonzero_count: Option<usize>,
367 #[serde(default, skip_serializing_if = "Option::is_none")]
369 pub sparse_candidate_count: Option<usize>,
370 #[serde(default, skip_serializing_if = "Vec::is_empty")]
372 pub sparse_representations: Vec<String>,
373 #[serde(default, skip_serializing_if = "Vec::is_empty")]
375 pub sparse_result_ranks: Vec<SparseRankReceiptV1>,
376 pub fallback: Option<String>,
378 pub exact_rerank: bool,
380 pub result_ids: Vec<String>,
382 pub degradations: Vec<String>,
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
388pub struct SparseRankReceiptV1 {
389 pub result_id: String,
391 pub rank: usize,
393}
394
395#[derive(Debug, Clone, Serialize, Deserialize)]
397pub struct DerivedVectorArtifactGenerationV1 {
398 pub schema_version: String,
400 pub generation_id: String,
402 pub codec_family: String,
404 pub codec_profile_digest: String,
406 pub source_snapshot_digest: String,
408 pub source_row_count: usize,
410 pub artifact_count: usize,
412 pub source_tables: Vec<String>,
414 pub dim: usize,
416 pub encoding: String,
418 pub created_at: DateTime<Utc>,
420 pub build_receipt_id: Option<String>,
422 pub artifact_manifest_digest: String,
424 pub status: String,
426 pub degradations: Vec<String>,
428}
429
430#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
432pub struct ProveKvPoolGenerationV1 {
433 pub schema_version: String,
434 pub generation_id: String,
435 pub embedding_snapshot_digest: String,
436 pub source_digest: String,
437 pub pool_manifest_digest: String,
438 pub codec_family: String,
439 pub codec_profile: String,
440 pub vector_dim: usize,
441 pub item_count: usize,
442 pub payload_bytes: u64,
443 pub created_at: DateTime<Utc>,
444}
445
446#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
448pub struct ProveKvPoolItemMapEntryV1 {
449 pub generation_id: String,
450 pub item_id: String,
451 pub source_type: String,
452 pub pool_index: usize,
453 pub embedding_digest: String,
454}
455
456#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
458#[serde(rename_all = "snake_case")]
459pub enum ProveKvPoolGenerationStatus {
460 Disabled,
461 Missing,
462 Building,
463 Ready,
464 Stale,
465 Failed,
466}
467
468#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
470pub struct ProveKvPoolArtifactStatusV1 {
471 pub status: ProveKvPoolGenerationStatus,
472 pub generation_id: Option<String>,
473 pub embedding_snapshot_digest: Option<String>,
474 pub pool_manifest_digest: Option<String>,
475 pub item_count: usize,
476 pub payload_bytes: u64,
477 pub reason: Option<String>,
478}
479
480#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
482pub struct ProveKvPoolArtifactBuildReceiptV1 {
483 pub schema_version: String,
484 pub generation_id: String,
485 pub embedding_snapshot_digest: String,
486 pub source_digest: String,
487 pub pool_manifest_digest: String,
488 pub codec_family: String,
489 pub codec_profile: String,
490 pub vector_dim: usize,
491 pub item_count: usize,
492 pub payload_bytes: u64,
493 pub exact_rerank_required: bool,
494 pub created_at: DateTime<Utc>,
495}
496
497#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
499pub struct DerivedCandidateReceiptV1 {
500 pub candidate_backend: String,
501 pub codec_family: Option<String>,
502 pub generation_id: Option<String>,
503 pub embedding_snapshot_digest: Option<String>,
504 pub pool_manifest_digest: Option<String>,
505 pub exact_rerank: bool,
506 pub approximate: bool,
507 pub fallback: Option<String>,
508 pub raw_candidate_count: usize,
509 pub post_filter_count: usize,
510 pub final_result_count: usize,
511}
512
513#[derive(Debug, Clone, Serialize, Deserialize)]
515pub struct VectorArtifactBuildReceiptV1 {
516 pub schema_version: String,
518 pub codec_family: String,
520 pub codec_profile_digest: String,
522 pub source_row_count: usize,
524 pub artifact_count: usize,
526 #[serde(default, skip_serializing_if = "Option::is_none")]
528 pub generation_id: Option<String>,
529 #[serde(default, skip_serializing_if = "Option::is_none")]
531 pub source_snapshot_digest: Option<String>,
532 #[serde(default, skip_serializing_if = "Option::is_none")]
534 pub artifact_manifest_digest: Option<String>,
535 #[serde(default, skip_serializing_if = "Option::is_none")]
537 pub build_receipt_id: Option<String>,
538 pub skipped_row_count: usize,
540 pub elapsed_ms: u128,
542 pub created_at: DateTime<Utc>,
544 pub degradations: Vec<String>,
546}
547
548fn default_vector_search_receipt_schema() -> String {
549 "vector_search_receipt_v1".to_string()
550}
551
552#[derive(Debug, Clone, Serialize, Deserialize)]
554pub struct SearchReceiptAnswersV1 {
555 pub receipt_id: String,
557 pub replay_receipt_id: String,
559 pub evaluation_time: DateTime<Utc>,
561 pub search_profile: String,
563 pub candidate_backend: String,
565 pub codec_family: Option<String>,
567 pub codec_profile_digest: Option<String>,
569 pub exactness: String,
571 pub approximate: bool,
573 pub exact_rerank: bool,
575 pub fallback: Option<String>,
577 pub degraded: bool,
579 pub replay_ready: bool,
581 pub rebuild_ready: bool,
583 pub result_ids: Vec<String>,
585 pub result_count: usize,
587 pub degradations: Vec<String>,
589 pub why_results_appeared: Vec<String>,
591}
592
593impl VectorSearchReceiptV1 {
594 pub fn answers(&self) -> SearchReceiptAnswersV1 {
596 let exactness = match (self.approximate, self.exact_rerank) {
597 (true, true) => "approximate_candidate_generation_with_exact_rerank",
598 (true, false) => "approximate",
599 (false, true) => "exact_reference_with_rerank",
600 (false, false) => "exact_reference",
601 }
602 .to_string();
603
604 let mut why_results_appeared = Vec::new();
605 why_results_appeared.push(format!(
606 "retrieval used candidate backend '{}'",
607 self.candidate_backend
608 ));
609 if self.exact_rerank {
610 why_results_appeared.push("final vector ordering used exact f32 scoring".to_string());
611 }
612 if self.sparse_enabled {
613 why_results_appeared.push(format!(
614 "sparse dot-product retrieval admitted {} candidates",
615 self.sparse_candidate_count.unwrap_or(0)
616 ));
617 }
618 if let Some(fallback) = &self.fallback {
619 why_results_appeared.push(format!("fallback path '{}' was used", fallback));
620 }
621 if let Some(codec_profile_digest) = &self.codec_profile_digest {
622 why_results_appeared.push(format!(
623 "derived vector artifacts used codec profile '{}'",
624 codec_profile_digest
625 ));
626 } else {
627 why_results_appeared.push("no derived codec profile was used".to_string());
628 }
629 if let Some(query_embedding_digest) = &self.query_embedding_digest {
630 why_results_appeared.push(format!(
631 "query embedding digest '{}' is recorded for replay checks",
632 query_embedding_digest
633 ));
634 }
635
636 SearchReceiptAnswersV1 {
637 receipt_id: self.receipt_id.clone(),
638 replay_receipt_id: self.receipt_id.clone(),
639 evaluation_time: self.evaluation_time,
640 search_profile: self.search_profile.clone(),
641 candidate_backend: self.candidate_backend.clone(),
642 codec_family: self.codec_family.clone(),
643 codec_profile_digest: self.codec_profile_digest.clone(),
644 exactness,
645 approximate: self.approximate,
646 exact_rerank: self.exact_rerank,
647 fallback: self.fallback.clone(),
648 degraded: self.fallback.is_some() || !self.degradations.is_empty(),
649 replay_ready: self.query_embedding_digest.is_some(),
650 rebuild_ready: self.query_embedding_digest.is_some()
651 && self.exact_rerank
652 && self.fallback.is_none()
653 && (self
654 .vector_artifact_count
655 .or(self.artifact_count)
656 .is_some_and(|count| count > 0)
657 || (self.codec_family.is_none()
658 && self.candidate_backend.contains("brute_force_f32")
659 && !self.result_ids.is_empty())),
660 result_ids: self.result_ids.clone(),
661 result_count: self.result_ids.len(),
662 degradations: self.degradations.clone(),
663 why_results_appeared,
664 }
665 }
666}
667
668#[derive(Debug, Clone, Serialize, Deserialize)]
670pub struct SearchResponse {
671 pub results: Vec<SearchResult>,
673 pub receipt: Option<VectorSearchReceiptV1>,
675}
676
677#[derive(Debug, Clone, Serialize, Deserialize)]
682pub struct ChunkManifestEntry {
683 pub external_chunk_id: String,
685 pub content: String,
687 #[serde(default, skip_serializing_if = "Option::is_none")]
689 pub token_count_estimate: Option<usize>,
690 #[serde(default, skip_serializing_if = "Option::is_none")]
692 pub content_digest: Option<String>,
693 #[serde(default, skip_serializing_if = "Option::is_none")]
695 pub metadata: Option<serde_json::Value>,
696}
697
698#[derive(Debug, Clone, Serialize, Deserialize)]
700pub struct ChunkManifestIngestOptions {
701 pub title: String,
703 pub namespace: String,
705 #[serde(default, skip_serializing_if = "Option::is_none")]
707 pub source_path: Option<String>,
708 #[serde(default, skip_serializing_if = "Option::is_none")]
710 pub metadata: Option<serde_json::Value>,
711}
712
713#[derive(Debug, Clone, Serialize, Deserialize)]
715pub struct ChunkManifestChunkMapping {
716 pub external_chunk_id: String,
718 pub sm_document_id: String,
720 pub sm_chunk_id: String,
722 pub chunk_index: usize,
724 #[serde(default, skip_serializing_if = "Option::is_none")]
726 pub content_digest: Option<String>,
727 #[serde(default, skip_serializing_if = "Option::is_none")]
729 pub metadata: Option<serde_json::Value>,
730}
731
732#[derive(Debug, Clone, Serialize, Deserialize)]
734pub struct ChunkManifestIngestResult {
735 pub sm_document_id: String,
737 pub namespace: String,
739 pub receipt_id: String,
741 pub chunks: Vec<ChunkManifestChunkMapping>,
743}
744
745#[derive(Debug, Clone, Serialize, Deserialize)]
747pub struct ExplainedSearchResponse {
748 pub results: Vec<ExplainedResult>,
750 pub receipt: Option<VectorSearchReceiptV1>,
752}
753
754#[derive(Debug, Clone, Serialize, Deserialize)]
756pub struct SearchReplayReportV1 {
757 pub receipt_id: String,
759 pub replay_receipt_id: String,
761 pub original_receipt: VectorSearchReceiptV1,
763 pub replay_receipt: VectorSearchReceiptV1,
765 pub query_embedding_digest_matches: bool,
767 pub result_ids_match: bool,
769 pub missing_result_ids: Vec<String>,
771 pub added_result_ids: Vec<String>,
773 pub vector_only: bool,
775}
776
777#[derive(Debug, Clone, Serialize, Deserialize)]
779pub struct ProjectionQuery {
780 pub scope: ScopeKey,
782 #[serde(default, skip_serializing_if = "Option::is_none")]
784 pub text_query: Option<String>,
785 #[serde(default, skip_serializing_if = "Option::is_none")]
787 pub valid_at: Option<String>,
788 #[serde(default, skip_serializing_if = "Option::is_none")]
790 pub recorded_at_or_before: Option<String>,
791 #[serde(default, skip_serializing_if = "Option::is_none")]
793 pub subject_entity_id: Option<EntityId>,
794 #[serde(default, skip_serializing_if = "Option::is_none")]
796 pub canonical_entity_id: Option<EntityId>,
797 #[serde(default, skip_serializing_if = "Option::is_none")]
799 pub claim_state: Option<String>,
800 #[serde(default, skip_serializing_if = "Option::is_none")]
802 pub claim_id: Option<ClaimId>,
803 #[serde(default, skip_serializing_if = "Option::is_none")]
805 pub claim_version_id: Option<ClaimVersionId>,
806 pub limit: usize,
808}
809
810impl ProjectionQuery {
811 pub fn new(scope: ScopeKey) -> Self {
812 Self {
813 scope,
814 text_query: None,
815 valid_at: None,
816 recorded_at_or_before: None,
817 subject_entity_id: None,
818 canonical_entity_id: None,
819 claim_state: None,
820 claim_id: None,
821 claim_version_id: None,
822 limit: 10,
823 }
824 }
825}
826
827#[derive(Debug, Clone, Serialize, Deserialize)]
829pub struct ProjectionClaimVersion {
830 pub claim_version_id: ClaimVersionId,
831 pub claim_id: ClaimId,
832 pub claim_state: String,
833 pub projection_family: String,
834 pub subject_entity_id: EntityId,
835 pub predicate: String,
836 pub object_anchor: serde_json::Value,
837 pub scope_key: ScopeKey,
838 pub valid_from: Option<String>,
839 pub valid_to: Option<String>,
840 pub recorded_at: String,
841 pub preferred_open: bool,
842 pub source_envelope_id: EnvelopeId,
843 pub source_authority: String,
844 pub trace_id: Option<String>,
845 pub freshness: String,
846 pub contradiction_status: String,
847 pub supersedes_claim_version_id: Option<ClaimVersionId>,
848 pub content: String,
849 pub confidence: f32,
850 pub metadata: Option<serde_json::Value>,
851 pub source_exported_at: Option<String>,
852 pub transformed_at: Option<String>,
853}
854
855#[derive(Debug, Clone, Serialize, Deserialize)]
857pub struct ProjectionRelationVersion {
858 pub relation_version_id: RelationVersionId,
859 pub subject_entity_id: EntityId,
860 pub predicate: String,
861 pub object_anchor: serde_json::Value,
862 pub scope_key: ScopeKey,
863 pub claim_id: Option<ClaimId>,
864 pub source_episode_id: Option<EpisodeId>,
865 pub valid_from: Option<String>,
866 pub valid_to: Option<String>,
867 pub recorded_at: String,
868 pub preferred_open: bool,
869 pub supersedes_relation_version_id: Option<RelationVersionId>,
870 pub contradiction_status: String,
871 pub source_confidence: f32,
872 pub projection_family: String,
873 pub source_envelope_id: EnvelopeId,
874 pub source_authority: String,
875 pub trace_id: Option<String>,
876 pub freshness: String,
877 pub metadata: Option<serde_json::Value>,
878 pub source_exported_at: Option<String>,
879 pub transformed_at: Option<String>,
880}
881
882#[derive(Debug, Clone, Serialize, Deserialize)]
884pub struct ProjectionEpisode {
885 pub episode_id: EpisodeId,
886 pub document_id: String,
887 pub cause_ids: Vec<String>,
888 pub effect_type: String,
889 pub outcome: String,
890 pub confidence: f32,
891 pub experiment_id: Option<String>,
892 pub scope_key: ScopeKey,
893 pub source_envelope_id: EnvelopeId,
894 pub source_authority: String,
895 pub trace_id: Option<String>,
896 pub recorded_at: String,
897 pub metadata: Option<serde_json::Value>,
898 pub source_exported_at: Option<String>,
899 pub transformed_at: Option<String>,
900}
901
902#[derive(Debug, Clone, Serialize, Deserialize)]
904pub struct ProjectionEntityAlias {
905 pub canonical_entity_id: EntityId,
906 pub alias_text: String,
907 pub alias_source: String,
908 pub match_evidence: Option<serde_json::Value>,
909 pub confidence: f32,
910 pub merge_decision: String,
911 pub scope_key: ScopeKey,
912 pub review_state: String,
913 pub is_human_confirmed: bool,
914 pub is_human_confirmed_final: bool,
915 pub superseded_by_entity_id: Option<EntityId>,
916 pub split_from_entity_id: Option<EntityId>,
917 pub source_envelope_id: EnvelopeId,
918 pub recorded_at: String,
919 pub source_exported_at: Option<String>,
920 pub transformed_at: Option<String>,
921}
922
923#[derive(Debug, Clone, Serialize, Deserialize)]
925pub struct ProjectionEvidenceRef {
926 pub claim_id: ClaimId,
927 pub claim_version_id: Option<ClaimVersionId>,
928 pub fetch_handle: String,
929 pub source_authority: String,
930 pub source_envelope_id: EnvelopeId,
931 pub scope_key: ScopeKey,
932 pub recorded_at: String,
933 pub metadata: Option<serde_json::Value>,
934 pub source_exported_at: Option<String>,
935 pub transformed_at: Option<String>,
936}
937
938#[derive(Debug, Clone, Serialize, Deserialize)]
940pub struct Session {
941 pub id: String,
943 pub channel: String,
945 pub created_at: String,
947 pub updated_at: String,
949 pub metadata: Option<serde_json::Value>,
951 pub message_count: u32,
953}
954
955#[derive(Debug, Clone, Serialize, Deserialize)]
957pub struct Message {
958 pub id: i64,
960 pub session_id: String,
962 pub role: Role,
964 pub content: String,
966 pub token_count: Option<u32>,
968 pub created_at: String,
970 pub metadata: Option<serde_json::Value>,
972}
973
974#[derive(Debug, Clone, Serialize, Deserialize)]
976pub struct Fact {
977 pub id: String,
979 pub namespace: String,
981 pub content: String,
983 pub source: Option<String>,
985 pub created_at: String,
987 pub updated_at: String,
989 pub metadata: Option<serde_json::Value>,
991}
992
993#[derive(Debug, Clone, Serialize, Deserialize)]
995pub struct Document {
996 pub id: String,
998 pub title: String,
1000 pub source_path: Option<String>,
1002 pub namespace: String,
1004 pub created_at: String,
1006 pub metadata: Option<serde_json::Value>,
1008 pub chunk_count: u32,
1010}
1011
1012#[derive(Debug, Clone, Serialize, Deserialize)]
1014pub struct TextChunk {
1015 pub index: usize,
1017 pub content: String,
1019 pub token_count_estimate: usize,
1021}
1022
1023#[derive(Debug, Clone, Serialize, Deserialize)]
1025pub struct SearchResult {
1026 pub content: String,
1028
1029 pub source: SearchSource,
1031
1032 pub score: f64,
1034
1035 pub bm25_rank: Option<usize>,
1037
1038 pub vector_rank: Option<usize>,
1040
1041 pub cosine_similarity: Option<f64>,
1043}
1044
1045#[derive(Debug, Clone, Serialize, Deserialize)]
1047#[serde(rename_all = "snake_case")]
1048pub enum SearchSource {
1049 Fact {
1051 fact_id: String,
1053 namespace: String,
1055 },
1056 Chunk {
1058 chunk_id: String,
1060 document_id: String,
1062 document_title: String,
1064 chunk_index: usize,
1066 },
1067 Message {
1069 message_id: i64,
1071 session_id: String,
1073 role: String,
1075 },
1076 Episode {
1078 episode_id: String,
1081 document_id: String,
1083 effect_type: String,
1085 outcome: String,
1087 },
1088 Projection {
1090 projection_kind: String,
1092 projection_id: String,
1094 scope_key: ScopeKey,
1096 valid_from: Option<String>,
1098 valid_to: Option<String>,
1100 recorded_at: String,
1102 source_envelope_id: String,
1104 source_authority: String,
1106 },
1107}
1108
1109impl SearchSource {
1110 pub fn result_id(&self) -> String {
1112 match self {
1113 Self::Fact { fact_id, .. } => format!("fact:{fact_id}"),
1114 Self::Chunk { chunk_id, .. } => format!("chunk:{chunk_id}"),
1115 Self::Message { message_id, .. } => format!("msg:{message_id}"),
1116 Self::Episode { episode_id, .. } => format!("episode:{episode_id}"),
1117 Self::Projection { projection_id, .. } => format!("projection:{projection_id}"),
1118 }
1119 }
1120
1121 pub fn source_kind(&self) -> &'static str {
1123 match self {
1124 Self::Fact { .. } => "fact",
1125 Self::Chunk { .. } => "chunk",
1126 Self::Message { .. } => "message",
1127 Self::Episode { .. } => "episode",
1128 Self::Projection { .. } => "projection",
1129 }
1130 }
1131
1132 pub fn source_id(&self) -> String {
1134 match self {
1135 Self::Fact { fact_id, .. } => fact_id.clone(),
1136 Self::Chunk { chunk_id, .. } => chunk_id.clone(),
1137 Self::Message { message_id, .. } => message_id.to_string(),
1138 Self::Episode { episode_id, .. } => episode_id.clone(),
1139 Self::Projection { projection_id, .. } => projection_id.clone(),
1140 }
1141 }
1142}
1143
1144#[derive(Debug, Clone, Serialize, Deserialize)]
1148pub struct EpisodeMeta {
1149 pub cause_ids: Vec<String>,
1151 pub effect_type: String,
1153 pub outcome: EpisodeOutcome,
1155 pub confidence: f32,
1157 pub verification_status: VerificationStatus,
1159 pub experiment_id: Option<String>,
1161 pub valid_time: Option<chrono::DateTime<chrono::Utc>>,
1163 pub fact_digest: Option<String>,
1165}
1166
1167#[derive(Debug, Clone, Serialize, Deserialize)]
1169pub struct EpisodeAsOfReceiptV1 {
1170 pub query_id: String,
1171 pub as_of_valid: chrono::DateTime<chrono::Utc>,
1172 pub as_of_recorded: chrono::DateTime<chrono::Utc>,
1173 pub episode_count: usize,
1174 pub episode_ids: Vec<String>,
1175 pub excluded_superseded: usize,
1176}
1177
1178#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1180#[serde(rename_all = "lowercase")]
1181pub enum EpisodeOutcome {
1182 Confirmed,
1184 Refuted,
1186 Inconclusive,
1188 Pending,
1190}
1191
1192impl EpisodeOutcome {
1193 pub fn as_str(&self) -> &'static str {
1195 match self {
1196 Self::Confirmed => "confirmed",
1197 Self::Refuted => "refuted",
1198 Self::Inconclusive => "inconclusive",
1199 Self::Pending => "pending",
1200 }
1201 }
1202
1203 pub fn from_str_value(s: &str) -> Option<Self> {
1205 match s {
1206 "confirmed" => Some(Self::Confirmed),
1207 "refuted" => Some(Self::Refuted),
1208 "inconclusive" => Some(Self::Inconclusive),
1209 "pending" => Some(Self::Pending),
1210 _ => None,
1211 }
1212 }
1213}
1214
1215impl std::fmt::Display for EpisodeOutcome {
1216 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1217 f.write_str(self.as_str())
1218 }
1219}
1220
1221#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1223#[serde(tag = "status", rename_all = "lowercase")]
1224pub enum VerificationStatus {
1225 Unverified,
1227 Verified {
1229 method: String,
1231 at: String,
1233 },
1234 Failed {
1236 reason: String,
1238 at: String,
1240 },
1241}
1242
1243#[derive(Debug, Clone, Serialize, Deserialize)]
1247pub struct ScoreBreakdown {
1248 pub rrf_score: f64,
1250 pub bm25_score: Option<f64>,
1252 pub vector_score: Option<f64>,
1254 #[serde(default, skip_serializing_if = "Option::is_none")]
1256 pub sparse_score: Option<f64>,
1257 pub recency_score: Option<f64>,
1259 pub bm25_rank: Option<usize>,
1261 pub vector_rank: Option<usize>,
1263 #[serde(default, skip_serializing_if = "Option::is_none")]
1265 pub sparse_rank: Option<usize>,
1266 pub vector_source_rank: Option<usize>,
1268 pub vector_source_score: Option<f64>,
1270 pub bm25_contribution: Option<f64>,
1272 pub vector_contribution: Option<f64>,
1274 #[serde(default, skip_serializing_if = "Option::is_none")]
1276 pub sparse_contribution: Option<f64>,
1277 pub vector_reranked_from_f32: bool,
1279 pub bm25_weight: f64,
1281 pub vector_weight: f64,
1283 #[serde(default)]
1285 pub sparse_weight: f64,
1286 pub recency_weight: Option<f64>,
1288 pub rrf_k: f64,
1290}
1291
1292#[derive(Debug, Clone, Serialize, Deserialize)]
1294pub struct ExplainedResult {
1295 pub result: SearchResult,
1297 pub breakdown: ScoreBreakdown,
1299}
1300
1301#[derive(Debug, Clone, Serialize, Deserialize)]
1303pub struct ExplainedResultAnswerV1 {
1304 pub result_id: String,
1306 pub source_kind: String,
1308 pub source_id: String,
1310 pub why_this_result: Vec<String>,
1312 pub text_match: bool,
1314 pub vector_match: bool,
1316 pub recency_applied: bool,
1318 pub exact_vector_rerank: bool,
1320 pub final_score: f64,
1322}
1323
1324impl ExplainedResult {
1325 pub fn answer(&self) -> ExplainedResultAnswerV1 {
1327 let text_match = self.breakdown.bm25_rank.is_some();
1328 let vector_match = self.breakdown.vector_rank.is_some();
1329 let recency_applied = self.breakdown.recency_score.is_some();
1330 let mut why_this_result = Vec::new();
1331
1332 if let Some(rank) = self.breakdown.bm25_rank {
1333 why_this_result.push(format!("text match rank {rank} contributed to fusion"));
1334 }
1335 if let Some(rank) = self.breakdown.vector_rank {
1336 why_this_result.push(format!("vector match rank {rank} contributed to fusion"));
1337 }
1338 if let Some(rank) = self.breakdown.sparse_rank {
1339 why_this_result.push(format!("sparse match rank {rank} contributed to fusion"));
1340 }
1341 if recency_applied {
1342 why_this_result.push("recency contributed to the fused score".to_string());
1343 }
1344 if self.breakdown.vector_reranked_from_f32 {
1345 why_this_result.push("vector score was checked with exact f32 rerank".to_string());
1346 }
1347 if why_this_result.is_empty() {
1348 why_this_result.push("result survived filtering and deterministic ranking".to_string());
1349 }
1350
1351 ExplainedResultAnswerV1 {
1352 result_id: self.result.source.result_id(),
1353 source_kind: self.result.source.source_kind().to_string(),
1354 source_id: self.result.source.source_id(),
1355 why_this_result,
1356 text_match,
1357 vector_match,
1358 recency_applied,
1359 exact_vector_rerank: self.breakdown.vector_reranked_from_f32,
1360 final_score: self.result.score,
1361 }
1362 }
1363}
1364
1365pub trait GraphView: Send + Sync {
1369 fn neighbors(
1371 &self,
1372 node_id: &str,
1373 direction: GraphDirection,
1374 max_depth: usize,
1375 ) -> Result<Vec<GraphEdge>, MemoryError>;
1376
1377 fn path(
1379 &self,
1380 from: &str,
1381 to: &str,
1382 max_depth: usize,
1383 ) -> Result<Option<Vec<String>>, MemoryError>;
1384}
1385
1386#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1388pub enum GraphDirection {
1389 Outgoing,
1391 Incoming,
1393 Both,
1395}
1396
1397#[derive(Debug, Clone, Serialize, Deserialize)]
1399pub struct GraphEdge {
1400 pub source: String,
1402 pub target: String,
1404 pub edge_type: GraphEdgeType,
1406 pub weight: f64,
1408 pub metadata: Option<serde_json::Value>,
1410}
1411
1412#[derive(Debug, Clone, Serialize, Deserialize)]
1414#[serde(rename_all = "snake_case")]
1415pub enum GraphEdgeType {
1416 Semantic {
1418 cosine_similarity: f32,
1420 },
1421 Temporal {
1423 delta_secs: u64,
1425 },
1426 Causal {
1428 confidence: f32,
1430 evidence_ids: Vec<String>,
1432 },
1433 Entity {
1435 relation: String,
1437 },
1438}
1439
1440#[derive(Debug, Clone, Serialize, Deserialize)]
1442pub struct EmbeddingDisplacement {
1443 pub cosine_similarity: f32,
1445 pub euclidean_distance: f32,
1447 pub magnitude_a: f32,
1449 pub magnitude_b: f32,
1451}
1452
1453#[derive(Debug, Clone, Serialize, Deserialize)]
1455pub struct MemoryStats {
1456 pub total_facts: u64,
1458 pub total_documents: u64,
1460 pub total_chunks: u64,
1462 pub total_sessions: u64,
1464 pub total_messages: u64,
1466 pub database_size_bytes: u64,
1468 pub embedding_model: Option<String>,
1470 pub embedding_dimensions: Option<usize>,
1472}
1473
1474#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1476pub struct NamespaceDeleteReport {
1477 pub facts: usize,
1479 pub documents: usize,
1481 pub chunks: usize,
1483 pub messages: usize,
1485 pub sessions: usize,
1487 pub episodes: usize,
1489 pub projection_rows: usize,
1491 pub hnsw_ops: usize,
1493}