use kmp_application::{
ApplicationError, AskMemoryQuery, GetContextResult, MemoryAnswerPolicy as DomainAnswerPolicy,
MemoryCoordinateData, MemoryData, MemoryDimensionData, MemoryEntryData, MemoryEvidenceData,
MemoryIngestCommand, MemoryProvenanceData, WakeMemoryQuery,
};
use kmp_domain::{DimensionSelection, PortError, ResolutionTier};
use kmp_memory_api::{
ApiCapabilities, ApiError, CONTRACT_VERSION, MemoryAnswerPolicy, MemoryAskRequest,
MemoryDetailView, MemoryNodeView, MemoryRecallApi, MemoryRecallView, MemoryRecordApi,
MemoryRecordRequest, MemoryRelationshipView, MemoryTier, MemoryWakeRequest, RecordedMemoryView,
RenderedMemoryView,
};
use crate::EmbeddedKernel;
const CAPABILITIES: [&str; 3] = ["wake", "ask", "record"];
impl MemoryRecallApi for EmbeddedKernel {
fn capabilities(&self) -> ApiCapabilities {
ApiCapabilities::new(CONTRACT_VERSION, env!("CARGO_PKG_VERSION"), CAPABILITIES)
}
async fn wake(&self, request: MemoryWakeRequest) -> Result<MemoryRecallView, ApiError> {
let about = request.about.clone();
let query = WakeMemoryQuery {
about: request.about,
role: request.role,
intent: request.intent,
dimensions: dimensions(request.dimension_kinds, request.scoped_to_about),
token_budget: request.token_budget,
depth: request.depth,
max_tier: request.max_tier.map(tier),
max_entries: request.max_entries.map(|entries| entries as usize),
};
let result = self.service().wake(query).await.map_err(translate_error)?;
self.observe_recall_quality("kernel_wake", &result);
Ok(recall_view(about, &result))
}
async fn ask(&self, request: MemoryAskRequest) -> Result<MemoryRecallView, ApiError> {
let about = request.about.clone();
let query = AskMemoryQuery {
about: request.about,
question: request.question,
answer_policy: answer_policy(request.answer_policy),
dimensions: dimensions(request.dimension_kinds, request.scoped_to_about),
token_budget: request.token_budget,
depth: request.depth,
max_tier: request.max_tier.map(tier),
};
let result = self.service().ask(query).await.map_err(translate_error)?;
self.observe_recall_quality("kernel_ask", &result);
Ok(recall_view(about, &result))
}
}
impl EmbeddedKernel {
fn observe_recall_quality(&self, rpc: &str, result: &GetContextResult) {
self.quality_observer().observe(
&result.rendered.quality,
&kmp_domain::QualityObservationContext {
rpc: rpc.to_owned(),
root_node_id: result.bundle.root_node_id().as_str().to_owned(),
role: result.bundle.role().as_str().to_owned(),
},
);
}
}
impl MemoryRecordApi for EmbeddedKernel {
fn capabilities(&self) -> ApiCapabilities {
ApiCapabilities::new(CONTRACT_VERSION, env!("CARGO_PKG_VERSION"), CAPABILITIES)
}
async fn record(&self, request: MemoryRecordRequest) -> Result<RecordedMemoryView, ApiError> {
let outcome = self
.service()
.ingest(ingest_command(request))
.await
.map_err(translate_record_error)?;
Ok(RecordedMemoryView {
about: outcome.about,
memory_id: outcome.memory_id,
accepted_entries: outcome.accepted.entries,
accepted_relations: outcome.accepted.relations,
accepted_evidence: outcome.accepted.evidence,
read_after_write_ready: outcome.read_after_write_ready,
warnings: outcome.warnings,
})
}
}
fn ingest_command(request: MemoryRecordRequest) -> MemoryIngestCommand {
MemoryIngestCommand {
about: request.about,
memory: MemoryData {
dimensions: request
.dimensions
.into_iter()
.map(|dimension| MemoryDimensionData {
id: dimension.id,
kind: dimension.kind,
title: dimension.title,
metadata: dimension.metadata,
})
.collect(),
entries: request
.entries
.into_iter()
.map(|entry| MemoryEntryData {
id: entry.id,
kind: entry.kind,
text: entry.text,
coordinates: entry
.coordinates
.into_iter()
.map(|coordinate| MemoryCoordinateData {
dimension: coordinate.dimension,
scope_id: coordinate.scope_id,
occurred_at: coordinate.occurred_at,
observed_at: None,
ingested_at: None,
valid_from: None,
valid_until: None,
sequence: coordinate.sequence,
rank: coordinate.rank,
metadata: Default::default(),
})
.collect(),
metadata: entry.metadata,
})
.collect(),
relations: request
.relations
.into_iter()
.map(|relation| kmp_application::MemoryRelationData {
source_ref: relation.from,
target_ref: relation.to,
rel: relation.rel,
semantic_class: relation.semantic_class,
why: relation.why,
evidence: None,
confidence: relation.confidence,
sequence: relation.sequence,
motivation: None,
method: None,
decision_id: None,
caused_by_node_id: None,
coordinate: None,
})
.collect(),
evidence: request
.evidence
.into_iter()
.map(|evidence| MemoryEvidenceData {
id: evidence.id,
supports: evidence.supports,
text: evidence.text,
source: evidence.source,
time: evidence.time,
metadata: evidence.metadata,
})
.collect(),
},
provenance: request.provenance.map(|provenance| MemoryProvenanceData {
source_kind: provenance.source_kind,
source_agent: provenance.source_agent,
observed_at: provenance.observed_at,
correlation_id: provenance.correlation_id,
causation_id: provenance.causation_id,
}),
idempotency_key: request.idempotency_key,
dry_run: false,
}
}
fn translate_record_error(error: ApplicationError) -> ApiError {
match error {
ApplicationError::Ports(PortError::Conflict(reason)) => ApiError::Refused { reason },
other => translate_error(other),
}
}
fn dimensions(kinds: Vec<String>, scoped_to_about: bool) -> DimensionSelection {
let selection = if kinds.is_empty() {
DimensionSelection::all()
} else {
DimensionSelection::only(kinds)
};
if scoped_to_about {
selection.with_current_about_scope()
} else {
selection
}
}
fn tier(tier: MemoryTier) -> ResolutionTier {
match tier {
MemoryTier::Summary => ResolutionTier::L0Summary,
MemoryTier::CausalSpine => ResolutionTier::L1CausalSpine,
MemoryTier::EvidencePack => ResolutionTier::L2EvidencePack,
}
}
fn answer_policy(policy: MemoryAnswerPolicy) -> DomainAnswerPolicy {
match policy {
MemoryAnswerPolicy::EvidenceOrUnknown => DomainAnswerPolicy::EvidenceOrUnknown,
MemoryAnswerPolicy::ShowConflicts => DomainAnswerPolicy::ShowConflicts,
MemoryAnswerPolicy::BestEffort => DomainAnswerPolicy::BestEffort,
}
}
fn recall_view(about: String, result: &GetContextResult) -> MemoryRecallView {
let bundle = &result.bundle;
MemoryRecallView {
about,
revision: bundle.metadata().revision,
content_hash: bundle.metadata().content_hash.clone(),
root: node_view(bundle.root_node()),
neighbors: bundle.neighbor_nodes().iter().map(node_view).collect(),
relationships: bundle
.relationships()
.iter()
.map(|relationship| MemoryRelationshipView {
source_node_id: relationship.source_node_id().to_string(),
target_node_id: relationship.target_node_id().to_string(),
relationship_type: relationship.relationship_type().to_string(),
why: relationship
.explanation()
.rationale()
.map(ToOwned::to_owned),
evidence: relationship.explanation().evidence().map(ToOwned::to_owned),
})
.collect(),
details: bundle
.node_details()
.iter()
.map(|detail| MemoryDetailView {
node_id: detail.node_id().to_string(),
detail: detail.detail().to_string(),
content_hash: detail.content_hash().to_string(),
revision: detail.revision(),
})
.collect(),
rendered: RenderedMemoryView {
content: result.rendered.content.clone(),
content_hash: result.rendered.content_hash.clone(),
token_count: result.rendered.token_count,
quality: kmp_memory_api::MemoryQualityView {
raw_equivalent_tokens: result.rendered.quality.raw_equivalent_tokens(),
compression_ratio: result.rendered.quality.compression_ratio(),
causal_density: result.rendered.quality.causal_density(),
noise_ratio: result.rendered.quality.noise_ratio(),
detail_coverage: result.rendered.quality.detail_coverage(),
},
},
}
}
fn node_view(node: &kmp_domain::BundleNode) -> MemoryNodeView {
MemoryNodeView {
node_id: node.node_id().to_string(),
node_kind: node.node_kind().to_string(),
title: node.title().to_string(),
summary: node.summary().to_string(),
status: node.status().to_string(),
labels: node.labels().to_vec(),
properties: node.properties().clone(),
}
}
fn translate_error(error: ApplicationError) -> ApiError {
match error {
ApplicationError::NotFound(what) => ApiError::NotFound { what },
ApplicationError::Validation(reason) => ApiError::Refused { reason },
refused @ ApplicationError::Domain(_) => ApiError::Refused {
reason: refused.to_string(),
},
unavailable @ ApplicationError::Ports(_) => ApiError::Unavailable {
reason: unavailable.to_string(),
},
}
}