1use kmp_application::{
8 ApplicationError, AskMemoryQuery, GetContextResult, MemoryAnswerPolicy as DomainAnswerPolicy,
9 MemoryCoordinateData, MemoryData, MemoryDimensionData, MemoryEntryData, MemoryEvidenceData,
10 MemoryIngestCommand, MemoryProvenanceData, WakeMemoryQuery,
11};
12use kmp_domain::{DimensionSelection, PortError, ResolutionTier};
13use kmp_memory_api::{
14 ApiCapabilities, ApiError, CONTRACT_VERSION, MemoryAnswerPolicy, MemoryAskRequest,
15 MemoryDetailView, MemoryNodeView, MemoryRecallApi, MemoryRecallView, MemoryRecordApi,
16 MemoryRecordRequest, MemoryRelationshipView, MemoryTier, MemoryWakeRequest, RecordedMemoryView,
17 RenderedMemoryView,
18};
19
20use crate::EmbeddedKernel;
21
22const CAPABILITIES: [&str; 3] = ["wake", "ask", "record"];
27
28impl MemoryRecallApi for EmbeddedKernel {
29 fn capabilities(&self) -> ApiCapabilities {
30 ApiCapabilities::new(CONTRACT_VERSION, env!("CARGO_PKG_VERSION"), CAPABILITIES)
31 }
32
33 async fn wake(&self, request: MemoryWakeRequest) -> Result<MemoryRecallView, ApiError> {
34 let about = request.about.clone();
35 let query = WakeMemoryQuery {
36 about: request.about,
37 role: request.role,
38 intent: request.intent,
39 dimensions: dimensions(request.dimension_kinds, request.scoped_to_about),
40 token_budget: request.token_budget,
41 depth: request.depth,
42 max_tier: request.max_tier.map(tier),
43 max_entries: request.max_entries.map(|entries| entries as usize),
44 };
45 let result = self.service().wake(query).await.map_err(translate_error)?;
46 self.observe_recall_quality("kernel_wake", &result);
47 Ok(recall_view(about, &result))
48 }
49
50 async fn ask(&self, request: MemoryAskRequest) -> Result<MemoryRecallView, ApiError> {
51 let about = request.about.clone();
52 let query = AskMemoryQuery {
53 about: request.about,
54 question: request.question,
55 answer_policy: answer_policy(request.answer_policy),
56 dimensions: dimensions(request.dimension_kinds, request.scoped_to_about),
57 token_budget: request.token_budget,
58 depth: request.depth,
59 max_tier: request.max_tier.map(tier),
60 };
61 let result = self.service().ask(query).await.map_err(translate_error)?;
62 self.observe_recall_quality("kernel_ask", &result);
63 Ok(recall_view(about, &result))
64 }
65}
66
67impl EmbeddedKernel {
68 fn observe_recall_quality(&self, rpc: &str, result: &GetContextResult) {
76 self.quality_observer().observe(
77 &result.rendered.quality,
78 &kmp_domain::QualityObservationContext {
79 rpc: rpc.to_owned(),
80 root_node_id: result.bundle.root_node_id().as_str().to_owned(),
81 role: result.bundle.role().as_str().to_owned(),
82 },
83 );
84 }
85}
86
87impl MemoryRecordApi for EmbeddedKernel {
88 fn capabilities(&self) -> ApiCapabilities {
89 ApiCapabilities::new(CONTRACT_VERSION, env!("CARGO_PKG_VERSION"), CAPABILITIES)
90 }
91
92 async fn record(&self, request: MemoryRecordRequest) -> Result<RecordedMemoryView, ApiError> {
93 let outcome = self
94 .service()
95 .ingest(ingest_command(request))
96 .await
97 .map_err(translate_record_error)?;
98 Ok(RecordedMemoryView {
99 about: outcome.about,
100 memory_id: outcome.memory_id,
101 accepted_entries: outcome.accepted.entries,
102 accepted_relations: outcome.accepted.relations,
103 accepted_evidence: outcome.accepted.evidence,
104 read_after_write_ready: outcome.read_after_write_ready,
105 warnings: outcome.warnings,
106 })
107 }
108}
109
110fn ingest_command(request: MemoryRecordRequest) -> MemoryIngestCommand {
111 MemoryIngestCommand {
112 about: request.about,
113 memory: MemoryData {
114 dimensions: request
115 .dimensions
116 .into_iter()
117 .map(|dimension| MemoryDimensionData {
118 id: dimension.id,
119 kind: dimension.kind,
120 title: dimension.title,
121 metadata: dimension.metadata,
122 })
123 .collect(),
124 entries: request
125 .entries
126 .into_iter()
127 .map(|entry| MemoryEntryData {
128 id: entry.id,
129 kind: entry.kind,
130 text: entry.text,
131 coordinates: entry
132 .coordinates
133 .into_iter()
134 .map(|coordinate| MemoryCoordinateData {
135 dimension: coordinate.dimension,
136 scope_id: coordinate.scope_id,
137 occurred_at: coordinate.occurred_at,
138 observed_at: None,
139 ingested_at: None,
140 valid_from: None,
141 valid_until: None,
142 sequence: coordinate.sequence,
143 rank: coordinate.rank,
144 metadata: Default::default(),
145 })
146 .collect(),
147 metadata: entry.metadata,
148 })
149 .collect(),
150 relations: request
151 .relations
152 .into_iter()
153 .map(|relation| kmp_application::MemoryRelationData {
154 source_ref: relation.from,
155 target_ref: relation.to,
156 rel: relation.rel,
157 semantic_class: relation.semantic_class,
158 why: relation.why,
159 evidence: None,
160 confidence: relation.confidence,
161 sequence: relation.sequence,
162 motivation: None,
163 method: None,
164 decision_id: None,
165 caused_by_node_id: None,
166 coordinate: None,
167 })
168 .collect(),
169 evidence: request
170 .evidence
171 .into_iter()
172 .map(|evidence| MemoryEvidenceData {
173 id: evidence.id,
174 supports: evidence.supports,
175 text: evidence.text,
176 source: evidence.source,
177 time: evidence.time,
178 metadata: evidence.metadata,
179 })
180 .collect(),
181 },
182 provenance: request.provenance.map(|provenance| MemoryProvenanceData {
183 source_kind: provenance.source_kind,
184 source_agent: provenance.source_agent,
185 observed_at: provenance.observed_at,
186 correlation_id: provenance.correlation_id,
187 causation_id: provenance.causation_id,
188 }),
189 idempotency_key: request.idempotency_key,
190 dry_run: false,
191 }
192}
193
194fn translate_record_error(error: ApplicationError) -> ApiError {
202 match error {
203 ApplicationError::Ports(PortError::Conflict(reason)) => ApiError::Refused { reason },
204 other => translate_error(other),
205 }
206}
207
208fn dimensions(kinds: Vec<String>, scoped_to_about: bool) -> DimensionSelection {
209 let selection = if kinds.is_empty() {
210 DimensionSelection::all()
211 } else {
212 DimensionSelection::only(kinds)
213 };
214 if scoped_to_about {
215 selection.with_current_about_scope()
216 } else {
217 selection
218 }
219}
220
221fn tier(tier: MemoryTier) -> ResolutionTier {
222 match tier {
223 MemoryTier::Summary => ResolutionTier::L0Summary,
224 MemoryTier::CausalSpine => ResolutionTier::L1CausalSpine,
225 MemoryTier::EvidencePack => ResolutionTier::L2EvidencePack,
226 }
227}
228
229fn answer_policy(policy: MemoryAnswerPolicy) -> DomainAnswerPolicy {
230 match policy {
231 MemoryAnswerPolicy::EvidenceOrUnknown => DomainAnswerPolicy::EvidenceOrUnknown,
232 MemoryAnswerPolicy::ShowConflicts => DomainAnswerPolicy::ShowConflicts,
233 MemoryAnswerPolicy::BestEffort => DomainAnswerPolicy::BestEffort,
234 }
235}
236
237fn recall_view(about: String, result: &GetContextResult) -> MemoryRecallView {
238 let bundle = &result.bundle;
239 MemoryRecallView {
240 about,
241 revision: bundle.metadata().revision,
242 content_hash: bundle.metadata().content_hash.clone(),
243 root: node_view(bundle.root_node()),
244 neighbors: bundle.neighbor_nodes().iter().map(node_view).collect(),
245 relationships: bundle
246 .relationships()
247 .iter()
248 .map(|relationship| MemoryRelationshipView {
249 source_node_id: relationship.source_node_id().to_string(),
250 target_node_id: relationship.target_node_id().to_string(),
251 relationship_type: relationship.relationship_type().to_string(),
252 why: relationship
253 .explanation()
254 .rationale()
255 .map(ToOwned::to_owned),
256 evidence: relationship.explanation().evidence().map(ToOwned::to_owned),
257 })
258 .collect(),
259 details: bundle
260 .node_details()
261 .iter()
262 .map(|detail| MemoryDetailView {
263 node_id: detail.node_id().to_string(),
264 detail: detail.detail().to_string(),
265 content_hash: detail.content_hash().to_string(),
266 revision: detail.revision(),
267 })
268 .collect(),
269 rendered: RenderedMemoryView {
270 content: result.rendered.content.clone(),
271 content_hash: result.rendered.content_hash.clone(),
272 token_count: result.rendered.token_count,
273 quality: kmp_memory_api::MemoryQualityView {
274 raw_equivalent_tokens: result.rendered.quality.raw_equivalent_tokens(),
275 compression_ratio: result.rendered.quality.compression_ratio(),
276 causal_density: result.rendered.quality.causal_density(),
277 noise_ratio: result.rendered.quality.noise_ratio(),
278 detail_coverage: result.rendered.quality.detail_coverage(),
279 },
280 },
281 }
282}
283
284fn node_view(node: &kmp_domain::BundleNode) -> MemoryNodeView {
285 MemoryNodeView {
286 node_id: node.node_id().to_string(),
287 node_kind: node.node_kind().to_string(),
288 title: node.title().to_string(),
289 summary: node.summary().to_string(),
290 status: node.status().to_string(),
291 labels: node.labels().to_vec(),
292 properties: node.properties().clone(),
293 }
294}
295
296fn translate_error(error: ApplicationError) -> ApiError {
297 match error {
298 ApplicationError::NotFound(what) => ApiError::NotFound { what },
299 ApplicationError::Validation(reason) => ApiError::Refused { reason },
300 refused @ ApplicationError::Domain(_) => ApiError::Refused {
301 reason: refused.to_string(),
302 },
303 unavailable @ ApplicationError::Ports(_) => ApiError::Unavailable {
305 reason: unavailable.to_string(),
306 },
307 }
308}