Skip to main content

semantic_memory/
types.rs

1#![allow(deprecated)]
2
3use crate::error::MemoryError;
4use serde::{Deserialize, Serialize};
5use stack_ids::{
6    ClaimId, ClaimVersionId, EntityId, EnvelopeId, EpisodeId, RelationVersionId, ScopeKey,
7};
8
9/// Stable trace identifier used for cross-crate correlation and auditability.
10///
11/// ## Phase status: compatibility / migration-only
12///
13/// This is a crate-local `TraceId` retained for backward compatibility.
14/// The canonical replacement is `stack_ids::TraceCtx`. Use
15/// `TraceCtx::from_legacy_trace_id()` to convert.
16///
17/// **Removal condition**: removed when all internal usage migrates to `TraceCtx`.
18#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
19#[serde(transparent)]
20pub struct CompatTraceId(pub String);
21
22#[deprecated(since = "0.5.0", note = "Use stack_ids::TraceCtx instead")]
23pub type TraceId = CompatTraceId;
24
25impl CompatTraceId {
26    /// Create a trace ID from any owned string-like input.
27    pub fn new(value: impl Into<String>) -> Self {
28        Self(value.into())
29    }
30
31    /// Borrow the trace ID as a string slice.
32    pub fn as_str(&self) -> &str {
33        &self.0
34    }
35}
36
37impl std::fmt::Display for CompatTraceId {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        f.write_str(&self.0)
40    }
41}
42
43impl From<String> for CompatTraceId {
44    fn from(value: String) -> Self {
45        Self(value)
46    }
47}
48
49impl From<&str> for CompatTraceId {
50    fn from(value: &str) -> Self {
51        Self(value.to_string())
52    }
53}
54
55/// Role of a message in a conversation.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "lowercase")]
58pub enum Role {
59    /// System prompt / instructions.
60    System,
61    /// User message.
62    User,
63    /// Assistant (LLM) response.
64    Assistant,
65    /// Tool call result.
66    Tool,
67}
68
69impl Role {
70    /// Convert to the string stored in SQLite.
71    pub fn as_str(&self) -> &'static str {
72        match self {
73            Role::System => "system",
74            Role::User => "user",
75            Role::Assistant => "assistant",
76            Role::Tool => "tool",
77        }
78    }
79
80    /// Parse from the string stored in SQLite.
81    pub fn from_str_value(s: &str) -> Option<Self> {
82        match s {
83            "system" => Some(Role::System),
84            "user" => Some(Role::User),
85            "assistant" => Some(Role::Assistant),
86            "tool" => Some(Role::Tool),
87            _ => None,
88        }
89    }
90}
91
92impl std::fmt::Display for Role {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        f.write_str(self.as_str())
95    }
96}
97
98impl std::str::FromStr for Role {
99    type Err = MemoryError;
100
101    fn from_str(s: &str) -> Result<Self, Self::Err> {
102        Self::from_str_value(s).ok_or_else(|| MemoryError::Other(format!("Unknown role: '{}'", s)))
103    }
104}
105
106/// Indicates whether a search result came from a fact, document chunk, message, or episode.
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum SearchSourceType {
109    /// Result is from the facts table.
110    Facts,
111    /// Result is from the chunks table.
112    Chunks,
113    /// Result is from the messages table.
114    Messages,
115    /// Result is from the episodes table.
116    Episodes,
117}
118
119/// Common filter surface for imported projection queries.
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct ProjectionQuery {
122    /// Full scope to enforce.
123    pub scope: ScopeKey,
124    /// Optional free-text query applied to the projection's searchable fields.
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub text_query: Option<String>,
127    /// Valid-time as-of filter for versioned projection rows.
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub valid_at: Option<String>,
130    /// Transaction-time cutoff for imported rows.
131    #[serde(default, skip_serializing_if = "Option::is_none")]
132    pub recorded_at_or_before: Option<String>,
133    /// Optional subject-entity filter for claim/relation queries.
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub subject_entity_id: Option<EntityId>,
136    /// Optional canonical-entity filter for alias queries.
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub canonical_entity_id: Option<EntityId>,
139    /// Optional claim-state filter for claim-version queries.
140    #[serde(default, skip_serializing_if = "Option::is_none")]
141    pub claim_state: Option<String>,
142    /// Optional claim filter for claim/evidence queries.
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub claim_id: Option<ClaimId>,
145    /// Optional claim-version filter for evidence queries.
146    #[serde(default, skip_serializing_if = "Option::is_none")]
147    pub claim_version_id: Option<ClaimVersionId>,
148    /// Final result limit.
149    pub limit: usize,
150}
151
152impl ProjectionQuery {
153    pub fn new(scope: ScopeKey) -> Self {
154        Self {
155            scope,
156            text_query: None,
157            valid_at: None,
158            recorded_at_or_before: None,
159            subject_entity_id: None,
160            canonical_entity_id: None,
161            claim_state: None,
162            claim_id: None,
163            claim_version_id: None,
164            limit: 10,
165        }
166    }
167}
168
169/// Public read shape for imported claim projection rows.
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct ProjectionClaimVersion {
172    pub claim_version_id: ClaimVersionId,
173    pub claim_id: ClaimId,
174    pub claim_state: String,
175    pub projection_family: String,
176    pub subject_entity_id: EntityId,
177    pub predicate: String,
178    pub object_anchor: serde_json::Value,
179    pub scope_key: ScopeKey,
180    pub valid_from: Option<String>,
181    pub valid_to: Option<String>,
182    pub recorded_at: String,
183    pub preferred_open: bool,
184    pub source_envelope_id: EnvelopeId,
185    pub source_authority: String,
186    pub trace_id: Option<String>,
187    pub freshness: String,
188    pub contradiction_status: String,
189    pub supersedes_claim_version_id: Option<ClaimVersionId>,
190    pub content: String,
191    pub confidence: f32,
192    pub metadata: Option<serde_json::Value>,
193    pub source_exported_at: Option<String>,
194    pub transformed_at: Option<String>,
195}
196
197/// Public read shape for imported relation projection rows.
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct ProjectionRelationVersion {
200    pub relation_version_id: RelationVersionId,
201    pub subject_entity_id: EntityId,
202    pub predicate: String,
203    pub object_anchor: serde_json::Value,
204    pub scope_key: ScopeKey,
205    pub claim_id: Option<ClaimId>,
206    pub source_episode_id: Option<EpisodeId>,
207    pub valid_from: Option<String>,
208    pub valid_to: Option<String>,
209    pub recorded_at: String,
210    pub preferred_open: bool,
211    pub supersedes_relation_version_id: Option<RelationVersionId>,
212    pub contradiction_status: String,
213    pub source_confidence: f32,
214    pub projection_family: String,
215    pub source_envelope_id: EnvelopeId,
216    pub source_authority: String,
217    pub trace_id: Option<String>,
218    pub freshness: String,
219    pub metadata: Option<serde_json::Value>,
220    pub source_exported_at: Option<String>,
221    pub transformed_at: Option<String>,
222}
223
224/// Public read shape for imported episode projection rows.
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct ProjectionEpisode {
227    pub episode_id: EpisodeId,
228    pub document_id: String,
229    pub cause_ids: Vec<String>,
230    pub effect_type: String,
231    pub outcome: String,
232    pub confidence: f32,
233    pub experiment_id: Option<String>,
234    pub scope_key: ScopeKey,
235    pub source_envelope_id: EnvelopeId,
236    pub source_authority: String,
237    pub trace_id: Option<String>,
238    pub recorded_at: String,
239    pub metadata: Option<serde_json::Value>,
240    pub source_exported_at: Option<String>,
241    pub transformed_at: Option<String>,
242}
243
244/// Public read shape for imported entity-alias rows.
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct ProjectionEntityAlias {
247    pub canonical_entity_id: EntityId,
248    pub alias_text: String,
249    pub alias_source: String,
250    pub match_evidence: Option<serde_json::Value>,
251    pub confidence: f32,
252    pub merge_decision: String,
253    pub scope_key: ScopeKey,
254    pub review_state: String,
255    pub is_human_confirmed: bool,
256    pub is_human_confirmed_final: bool,
257    pub superseded_by_entity_id: Option<EntityId>,
258    pub split_from_entity_id: Option<EntityId>,
259    pub source_envelope_id: EnvelopeId,
260    pub recorded_at: String,
261    pub source_exported_at: Option<String>,
262    pub transformed_at: Option<String>,
263}
264
265/// Public read shape for imported evidence-reference rows.
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct ProjectionEvidenceRef {
268    pub claim_id: ClaimId,
269    pub claim_version_id: Option<ClaimVersionId>,
270    pub fetch_handle: String,
271    pub source_authority: String,
272    pub source_envelope_id: EnvelopeId,
273    pub scope_key: ScopeKey,
274    pub recorded_at: String,
275    pub metadata: Option<serde_json::Value>,
276    pub source_exported_at: Option<String>,
277    pub transformed_at: Option<String>,
278}
279
280/// A conversation session.
281#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct Session {
283    /// UUID v4.
284    pub id: String,
285    /// Channel identifier (e.g. "repl", "telegram").
286    pub channel: String,
287    /// ISO 8601 timestamp.
288    pub created_at: String,
289    /// ISO 8601 timestamp.
290    pub updated_at: String,
291    /// Optional JSON metadata.
292    pub metadata: Option<serde_json::Value>,
293    /// Number of messages (populated on list queries).
294    pub message_count: u32,
295}
296
297/// A single message within a session.
298#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct Message {
300    /// Auto-increment ID.
301    pub id: i64,
302    /// Session this message belongs to.
303    pub session_id: String,
304    /// Role of the speaker.
305    pub role: Role,
306    /// Message text.
307    pub content: String,
308    /// Estimated token count (caller-provided).
309    pub token_count: Option<u32>,
310    /// ISO 8601 timestamp.
311    pub created_at: String,
312    /// Optional JSON metadata.
313    pub metadata: Option<serde_json::Value>,
314}
315
316/// A discrete fact in the knowledge store.
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct Fact {
319    /// UUID v4.
320    pub id: String,
321    /// Categorization namespace.
322    pub namespace: String,
323    /// The fact text.
324    pub content: String,
325    /// Where this fact came from.
326    pub source: Option<String>,
327    /// ISO 8601 timestamp.
328    pub created_at: String,
329    /// ISO 8601 timestamp.
330    pub updated_at: String,
331    /// Optional JSON metadata.
332    pub metadata: Option<serde_json::Value>,
333}
334
335/// A source document that has been chunked and embedded.
336#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct Document {
338    /// UUID v4.
339    pub id: String,
340    /// Document title.
341    pub title: String,
342    /// File path, URL, or identifier.
343    pub source_path: Option<String>,
344    /// Categorization namespace.
345    pub namespace: String,
346    /// ISO 8601 timestamp.
347    pub created_at: String,
348    /// Optional JSON metadata.
349    pub metadata: Option<serde_json::Value>,
350    /// Number of chunks (populated on list queries).
351    pub chunk_count: u32,
352}
353
354/// A chunk produced by the text splitter.
355#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct TextChunk {
357    /// Position in the original document (0-based).
358    pub index: usize,
359    /// The chunk text.
360    pub content: String,
361    /// Rough token estimate (chars / 4).
362    pub token_count_estimate: usize,
363}
364
365/// A single search result.
366#[derive(Debug, Clone, Serialize, Deserialize)]
367pub struct SearchResult {
368    /// The matched text content.
369    pub content: String,
370
371    /// Where this result came from.
372    pub source: SearchSource,
373
374    /// Combined RRF score. Higher = more relevant.
375    pub score: f64,
376
377    /// BM25 rank (1-based) if this result appeared in BM25 results.
378    pub bm25_rank: Option<usize>,
379
380    /// Vector rank (1-based) if this result appeared in vector results.
381    pub vector_rank: Option<usize>,
382
383    /// Cosine similarity score if computed.
384    pub cosine_similarity: Option<f64>,
385}
386
387/// Source information for a search result.
388#[derive(Debug, Clone, Serialize, Deserialize)]
389#[serde(rename_all = "snake_case")]
390pub enum SearchSource {
391    /// Result came from the facts table.
392    Fact {
393        /// Fact UUID.
394        fact_id: String,
395        /// Fact namespace.
396        namespace: String,
397    },
398    /// Result came from a document chunk.
399    Chunk {
400        /// Chunk UUID.
401        chunk_id: String,
402        /// Parent document UUID.
403        document_id: String,
404        /// Parent document title.
405        document_title: String,
406        /// Position within the document (0-based).
407        chunk_index: usize,
408    },
409    /// Result came from a conversation message.
410    Message {
411        /// Message auto-increment ID.
412        message_id: i64,
413        /// Session UUID.
414        session_id: String,
415        /// Message role (user, assistant, etc.).
416        role: String,
417    },
418    /// Result came from an episode (causal record). SearchSource::Episode variant.
419    Episode {
420        /// First-class episode identity (V9+). Falls back to `document_id + "-ep0"`
421        /// for legacy data.
422        episode_id: String,
423        /// Document ID the episode is attached to.
424        document_id: String,
425        /// Type of effect (e.g. "test_failure", "regression").
426        effect_type: String,
427        /// Current outcome.
428        outcome: String,
429    },
430    /// Result came from an imported projection row.
431    Projection {
432        /// Projection row family, such as `claim_version` or `relation_version`.
433        projection_kind: String,
434        /// Stable projection-row identity.
435        projection_id: String,
436        /// Full scope carried by the imported row.
437        scope_key: ScopeKey,
438        /// Validity start for versioned projections, if any.
439        valid_from: Option<String>,
440        /// Validity end for versioned projections, if any.
441        valid_to: Option<String>,
442        /// Authoritative importer-assigned recorded_at.
443        recorded_at: String,
444        /// Source envelope provenance.
445        source_envelope_id: String,
446        /// Source authority provenance.
447        source_authority: String,
448    },
449}
450
451// ─── Episode Types ─────────────────────────────────────────────
452
453/// Metadata for a causal episode (PRIMITIVES_CONTRACT §4).
454#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct EpisodeMeta {
456    /// IDs of the facts/chunks/messages that caused this episode.
457    pub cause_ids: Vec<String>,
458    /// Type of effect (e.g. "test_failure", "regression", "improvement").
459    pub effect_type: String,
460    /// Current outcome assessment.
461    pub outcome: EpisodeOutcome,
462    /// Confidence in the causal link (0.0 to 1.0).
463    pub confidence: f32,
464    /// Verification status.
465    pub verification_status: VerificationStatus,
466    /// Links to an EvidenceBundle.run_id (if experimentally verified).
467    pub experiment_id: Option<String>,
468}
469
470/// Outcome of an episode's causal hypothesis.
471#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
472#[serde(rename_all = "lowercase")]
473pub enum EpisodeOutcome {
474    /// Causal link confirmed by experiment.
475    Confirmed,
476    /// Causal link refuted by experiment.
477    Refuted,
478    /// Evidence is inconclusive.
479    Inconclusive,
480    /// Not yet tested.
481    Pending,
482}
483
484impl EpisodeOutcome {
485    /// Convert to the string stored in SQLite.
486    pub fn as_str(&self) -> &'static str {
487        match self {
488            Self::Confirmed => "confirmed",
489            Self::Refuted => "refuted",
490            Self::Inconclusive => "inconclusive",
491            Self::Pending => "pending",
492        }
493    }
494
495    /// Parse from the string stored in SQLite.
496    pub fn from_str_value(s: &str) -> Option<Self> {
497        match s {
498            "confirmed" => Some(Self::Confirmed),
499            "refuted" => Some(Self::Refuted),
500            "inconclusive" => Some(Self::Inconclusive),
501            "pending" => Some(Self::Pending),
502            _ => None,
503        }
504    }
505}
506
507impl std::fmt::Display for EpisodeOutcome {
508    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
509        f.write_str(self.as_str())
510    }
511}
512
513/// Verification status for an episode.
514#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
515#[serde(tag = "status", rename_all = "lowercase")]
516pub enum VerificationStatus {
517    /// Not yet verified.
518    Unverified,
519    /// Successfully verified.
520    Verified {
521        /// Method used for verification.
522        method: String,
523        /// When verification occurred (ISO 8601).
524        at: String,
525    },
526    /// Verification attempt failed.
527    Failed {
528        /// Reason for failure.
529        reason: String,
530        /// When verification was attempted (ISO 8601).
531        at: String,
532    },
533}
534
535// ─── Score Breakdown ───────────────────────────────────────────
536
537/// Detailed score breakdown for explainable search results.
538#[derive(Debug, Clone, Serialize, Deserialize)]
539pub struct ScoreBreakdown {
540    /// Final fused RRF score.
541    pub rrf_score: f64,
542    /// Raw BM25 score reported by SQLite FTS5 (lower is better).
543    pub bm25_score: Option<f64>,
544    /// Raw vector similarity used for the final vector ordering.
545    pub vector_score: Option<f64>,
546    /// Recency contribution added during fusion.
547    pub recency_score: Option<f64>,
548    /// BM25 rank (1-based).
549    pub bm25_rank: Option<usize>,
550    /// Vector rank (1-based).
551    pub vector_rank: Option<usize>,
552    /// Rank from the underlying vector retrieval source before any exact rerank.
553    pub vector_source_rank: Option<usize>,
554    /// Similarity score from the underlying vector retrieval source before rerank.
555    pub vector_source_score: Option<f64>,
556    /// BM25 RRF contribution to the final score.
557    pub bm25_contribution: Option<f64>,
558    /// Vector RRF contribution to the final score.
559    pub vector_contribution: Option<f64>,
560    /// Whether the vector ordering was reranked with exact f32 cosine similarity.
561    pub vector_reranked_from_f32: bool,
562    /// Configured BM25 fusion weight.
563    pub bm25_weight: f64,
564    /// Configured vector fusion weight.
565    pub vector_weight: f64,
566    /// Configured recency weight when recency is enabled.
567    pub recency_weight: Option<f64>,
568    /// Configured RRF decay constant.
569    pub rrf_k: f64,
570}
571
572/// Search result with full score explanation.
573#[derive(Debug, Clone, Serialize, Deserialize)]
574pub struct ExplainedResult {
575    /// The search result.
576    pub result: SearchResult,
577    /// Score breakdown.
578    pub breakdown: ScoreBreakdown,
579}
580
581// ─── Graph Types (PRIMITIVES_CONTRACT §8) ──────────────────────
582
583/// Trait for querying the memory store as a graph.
584pub trait GraphView: Send + Sync {
585    /// Find neighboring nodes up to `max_depth` hops away.
586    fn neighbors(
587        &self,
588        node_id: &str,
589        direction: GraphDirection,
590        max_depth: usize,
591    ) -> Result<Vec<GraphEdge>, MemoryError>;
592
593    /// Find a path between two nodes (BFS, max depth).
594    fn path(
595        &self,
596        from: &str,
597        to: &str,
598        max_depth: usize,
599    ) -> Result<Option<Vec<String>>, MemoryError>;
600}
601
602/// Direction for graph traversal.
603#[derive(Debug, Clone, Copy, PartialEq, Eq)]
604pub enum GraphDirection {
605    /// Follow outgoing edges.
606    Outgoing,
607    /// Follow incoming edges.
608    Incoming,
609    /// Follow edges in both directions.
610    Both,
611}
612
613/// An edge in the memory graph.
614#[derive(Debug, Clone, Serialize, Deserialize)]
615pub struct GraphEdge {
616    /// Source node ID.
617    pub source: String,
618    /// Target node ID.
619    pub target: String,
620    /// Type of relationship.
621    pub edge_type: GraphEdgeType,
622    /// Edge weight (interpretation depends on edge_type).
623    pub weight: f64,
624    /// Optional metadata.
625    pub metadata: Option<serde_json::Value>,
626}
627
628/// Type of relationship between graph nodes.
629#[derive(Debug, Clone, Serialize, Deserialize)]
630#[serde(rename_all = "snake_case")]
631pub enum GraphEdgeType {
632    /// Semantic similarity. GraphEdgeType::Semantic variant.
633    Semantic {
634        /// Cosine similarity between embeddings.
635        cosine_similarity: f32,
636    },
637    /// Temporal proximity. GraphEdgeType::Temporal variant.
638    Temporal {
639        /// Time delta in seconds.
640        delta_secs: u64,
641    },
642    /// Causal relationship. GraphEdgeType::Causal variant.
643    Causal {
644        /// Confidence in the causal link.
645        confidence: f32,
646        /// EvidenceBundle run_ids supporting this link.
647        evidence_ids: Vec<String>,
648    },
649    /// Entity co-occurrence. GraphEdgeType::Entity variant.
650    Entity {
651        /// Relationship type (e.g. "mentions", "modifies").
652        relation: String,
653    },
654}
655
656/// Embedding displacement between two text embeddings.
657#[derive(Debug, Clone, Serialize, Deserialize)]
658pub struct EmbeddingDisplacement {
659    /// Cosine similarity between the two embeddings.
660    pub cosine_similarity: f32,
661    /// Euclidean distance between the two embeddings.
662    pub euclidean_distance: f32,
663    /// Magnitude of the first embedding.
664    pub magnitude_a: f32,
665    /// Magnitude of the second embedding.
666    pub magnitude_b: f32,
667}
668
669/// Database statistics.
670#[derive(Debug, Clone, Serialize, Deserialize)]
671pub struct MemoryStats {
672    /// Total number of facts.
673    pub total_facts: u64,
674    /// Total number of documents.
675    pub total_documents: u64,
676    /// Total number of chunks across all documents.
677    pub total_chunks: u64,
678    /// Total number of conversation sessions.
679    pub total_sessions: u64,
680    /// Total number of messages across all sessions.
681    pub total_messages: u64,
682    /// Database file size in bytes.
683    pub database_size_bytes: u64,
684    /// Currently configured embedding model.
685    pub embedding_model: Option<String>,
686    /// Currently configured embedding dimensions.
687    pub embedding_dimensions: Option<usize>,
688}