hirn_engine/observability/
trace.rs1use hirn_core::id::MemoryId;
4use hirn_core::provenance::Provenance;
5use hirn_core::record::MemoryRecord;
6use hirn_core::types::{AgentId, Namespace, Origin};
7use hirn_core::{HirnError, HirnResult};
8
9use crate::causal::TraceReport;
10use crate::db::HirnDB;
11use crate::ql::context::ConflictGroup;
12use crate::ql::results::SemanticRevisionSummary;
13use crate::retrieval::recall::ResourceEvidenceSummary;
14
15#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
17pub struct TraceResult {
18 pub record: MemoryRecord,
20 pub provenance: Provenance,
22 pub source_episodes: Vec<MemoryId>,
24 pub derived_records: Vec<MemoryId>,
26 pub mutation_count: usize,
28 pub trust_score: f32,
30 pub lineage_tree: String,
32 pub semantic_revision: Option<SemanticRevisionSummary>,
34 pub conflict_groups: Vec<ConflictGroup>,
36 pub resource_evidence: Vec<ResourceEvidenceSummary>,
38}
39
40impl From<TraceReport> for TraceResult {
41 fn from(report: TraceReport) -> Self {
42 Self {
43 record: report.record,
44 provenance: report.provenance,
45 source_episodes: report.source_episodes,
46 derived_records: report.derived_records,
47 mutation_count: report.mutation_count,
48 trust_score: report.trust_score,
49 lineage_tree: report.lineage_tree,
50 semantic_revision: None,
51 conflict_groups: Vec::new(),
52 resource_evidence: Vec::new(),
53 }
54 }
55}
56
57pub struct TraceBuilder<'a> {
67 db: &'a HirnDB,
68 id: MemoryId,
69 allowed_namespaces: Option<Vec<Namespace>>,
70 agent_id: Option<String>,
71 exact_conflict_target: bool,
72}
73
74impl<'a> TraceBuilder<'a> {
75 pub(crate) fn new(db: &'a HirnDB, id: MemoryId) -> Self {
76 Self {
77 db,
78 id,
79 allowed_namespaces: None,
80 agent_id: None,
81 exact_conflict_target: false,
82 }
83 }
84
85 #[must_use]
86 pub fn allowed_namespaces(mut self, allowed_namespaces: Vec<Namespace>) -> Self {
87 self.allowed_namespaces = Some(allowed_namespaces);
88 self
89 }
90
91 #[must_use]
92 pub fn agent_id(mut self, agent_id: impl Into<String>) -> Self {
93 self.agent_id = Some(agent_id.into());
94 self
95 }
96
97 #[must_use]
98 pub fn exact_conflict_target(mut self, exact_conflict_target: bool) -> Self {
99 self.exact_conflict_target = exact_conflict_target;
100 self
101 }
102
103 pub async fn execute(self) -> HirnResult<TraceResult> {
105 let record = self.db.get_memory(self.id).await?;
106 if let Some(allowed_namespaces) = self.allowed_namespaces.as_deref() {
107 let namespace = record.effective_namespace();
108 if !allowed_namespaces.contains(&namespace) {
109 return Err(HirnError::AccessDenied(format!(
110 "TRACE cannot access namespace '{}'",
111 namespace.as_str()
112 )));
113 }
114 }
115
116 let conflict_groups = if self.exact_conflict_target {
117 crate::ql::context::detect_conflicts_for_exact_record(
118 self.db,
119 &record,
120 self.allowed_namespaces.as_deref(),
121 )
122 .await
123 .groups
124 } else {
125 crate::ql::context::detect_conflicts_for_record(
126 self.db,
127 &record,
128 self.allowed_namespaces.as_deref(),
129 )
130 .await
131 .groups
132 };
133 let semantic_revision = match &record {
134 MemoryRecord::Semantic(record) => {
135 Some(crate::ql::results::load_semantic_revision_summary(self.db, record).await?)
136 }
137 _ => None,
138 };
139
140 let (provenance, source_episodes) = match &record {
141 MemoryRecord::Episodic(e) => (e.provenance.clone(), vec![]),
142 MemoryRecord::Semantic(s) => (s.provenance.clone(), s.source_episodes.clone()),
143 MemoryRecord::Working(_) => (
144 Provenance::with_origin(Origin::DirectObservation, AgentId::well_known("system")),
145 vec![],
146 ),
147 MemoryRecord::Procedural(p) => (p.provenance.clone(), p.source_episodes.clone()),
148 };
149
150 let report = crate::causal::build_trace_report(
151 self.db.graph_store(),
152 record,
153 provenance,
154 source_episodes,
155 )
156 .await?;
157
158 let mut result = TraceResult::from(report);
159 result.semantic_revision = semantic_revision;
160 result.conflict_groups = conflict_groups;
161 let agent_id = self.agent_id.as_deref().unwrap_or("anonymous");
162 result.resource_evidence = self
163 .db
164 .resource_evidence_summaries_for_record(&result.record, agent_id)
165 .await?;
166 Ok(result)
167 }
168}