Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion crates/tinymemory-api/src/null.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ use crate::provider::{
CodingSessionIngestRequest, CodingSessionSource, CoverWindowQuery, EntityMatch, FacetType,
FastRetrieveQuery, MemoryChunks, MemoryCodingSessions, MemoryCore, MemoryDiff, MemoryDocuments,
MemoryEntities, MemoryGoals, MemoryGraph, MemoryIngest, MemoryMaintenance, MemoryPeople,
MemoryPortability, MemoryProfile, MemoryProvider, MemoryRecall, MemoryRetrieval,
MemoryPortability, MemoryProfile, MemoryProvider, MemoryRecall, MemoryRetrieval, MemoryScoring,
MemorySourceSink, MemorySourceSync, MemoryToolMemory, MemoryTree, PersonHandle,
PersonInteraction, PersonRecord, PersonScore, ProfileFacet, RankedPerson, RawArchiveCoverage,
RawRebuildOutcome, ResolvedPerson, RetrievalHit, RetrievalResponse, SourceRetrievalQuery,
Expand Down Expand Up @@ -785,6 +785,21 @@ impl MemoryCodingSessions for NullMemoryProvider {
}
}

#[async_trait]
impl MemoryScoring for NullMemoryProvider {
async fn extract_entities(&self, _query: &str) -> Result<Vec<String>, MemoryError> {
unsupported(Capability::Scoring)
}

async fn embed_text(&self, _text: &str) -> Result<Vec<f32>, MemoryError> {
unsupported(Capability::Scoring)
}

async fn embedder_slug(&self) -> Result<String, MemoryError> {
unsupported(Capability::Scoring)
}
}

#[cfg(test)]
#[path = "null_tests.rs"]
mod tests;
7 changes: 7 additions & 0 deletions crates/tinymemory-api/src/provider/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ use crate::provider::records::{
MemoryGoals, MemoryMaintenance, MemorySourceSink, MemoryToolMemory,
};
use crate::provider::retrieval::MemoryRetrieval;
use crate::provider::scoring::MemoryScoring;
use crate::provider::sessions::MemoryCodingSessions;
use crate::provider::sync::MemorySourceSync;

Expand Down Expand Up @@ -214,6 +215,11 @@ pub trait MemoryProvider: MemoryCore + MemoryRecall + MemoryPortability + 'stati
None
}

/// Scoring and NLP operations, when advertised.
fn as_scoring(&self) -> Option<&dyn MemoryScoring> {
None
}

/// Whether `capability` is actually **reachable** on this driver.
///
/// This is the implementation-side truth, as opposed to
Expand Down Expand Up @@ -246,6 +252,7 @@ pub trait MemoryProvider: MemoryCore + MemoryRecall + MemoryPortability + 'stati
Capability::Episodic => self.as_episodic().is_some(),
Capability::SourceSync => self.as_source_sync().is_some(),
Capability::CodingSessions => self.as_coding_sessions().is_some(),
Capability::Scoring => self.as_scoring().is_some(),
}
}
}
2 changes: 2 additions & 0 deletions crates/tinymemory-api/src/provider/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ pub mod people;
pub mod profile;
pub mod records;
pub mod retrieval;
pub mod scoring;
pub mod sessions;
pub mod sync;
// The value types every family exchanges, defined in `tinymemory-bus` and
Expand Down Expand Up @@ -109,6 +110,7 @@ pub use retrieval::{
CoverWindowQuery, EntityMatch, FastRetrieveQuery, MemoryRetrieval, RetrievalHit,
RetrievalNodeKind, RetrievalResponse, SourceRetrievalQuery,
};
pub use scoring::MemoryScoring;
pub use sessions::{
CodingSessionIngestReport, CodingSessionIngestRequest, CodingSessionSource,
MemoryCodingSessions,
Expand Down
61 changes: 61 additions & 0 deletions crates/tinymemory-api/src/provider/scoring.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//! [`MemoryScoring`] — scoring and NLP operations exposed over the bus.
//!
//! This family carries the three operations that currently keep
//! `tinymemory-core` in the host's build graph: entity extraction, text
//! embedding, and embedder identification. Moving them behind the bus lets
//! every call site that reached the engine directly for these purposes route
//! through the contract instead.
//!
//! ## Design note — why the host requests, not constructs
//!
//! The host previously constructed an embedder from config and called it
//! directly. That pattern cannot cross the bus: config is host-side, the
//! embedder lives in the module. The correct shape is that the host asks the
//! driver to perform the operation by intent (`embed_text`) and to identify
//! which provider is active (`embedder_slug`), delegating both the construction
//! and the execution to the driver.

use async_trait::async_trait;

use crate::error::MemoryError;

/// Scoring and NLP operations exposed over the bus.
#[async_trait]
pub trait MemoryScoring: Send + Sync {
/// Extract canonical entity strings from a natural-language query.
///
/// Returns `"<kind>:<value>"` strings in the same namespace as the indexed
/// chunk entities. An empty result means the query is ungrounded — no
/// entity anchors were found — which routes retrieval toward the global
/// (dense) branch rather than the entity-indexed branch.
///
/// Never fails: when the NLP backend is unavailable the implementation
/// degrades to a regex extractor rather than returning an error.
///
/// # Errors
///
/// Only infrastructure failures (e.g. the module bus is down). The NLP
/// step itself never errors — it degrades gracefully.
async fn extract_entities(&self, query: &str) -> Result<Vec<String>, MemoryError>;

/// Embed a text string with the active embedder.
///
/// Returns a float vector; the length matches the active embedding
/// dimension (currently 1024 for the default bge-m3 model).
///
/// # Errors
///
/// When no embedder is configured (`Unsupported`) or the embedding call
/// fails (e.g. the Ollama server is unreachable).
async fn embed_text(&self, text: &str) -> Result<Vec<f32>, MemoryError>;

/// Stable string identifying which embedder provider is currently active.
///
/// One of: `"ollama"`, `"none"`, `"custom"`, `"cloud"`, `"unconfigured"`.
/// Used by the host to decide how to attribute embedding costs in the UI.
///
/// # Errors
///
/// Only infrastructure failures. Config resolution itself never errors.
async fn embedder_slug(&self) -> Result<String, MemoryError>;
}
8 changes: 7 additions & 1 deletion crates/tinymemory-bus/src/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ pub enum Capability {
/// walk. Advertising them together would put a dead control in front of
/// whichever half is absent.
CodingSessions,
/// Scoring and NLP operations: entity extraction, text embedding, and
/// embedder identification.
Scoring,
}

impl Capability {
Expand All @@ -121,7 +124,7 @@ impl Capability {
/// Declaration order is also bit order in [`Capabilities`] and iteration
/// order in its serialized form, so this slice is the single ordering
/// authority for the whole module.
pub const ALL: [Capability; 20] = [
pub const ALL: [Capability; 21] = [
Capability::Core,
Capability::Recall,
Capability::Ingest,
Expand All @@ -145,6 +148,7 @@ impl Capability {
Capability::Episodic,
Capability::SourceSync,
Capability::CodingSessions,
Capability::Scoring,
];

/// The families a driver must advertise to be bindable at all.
Expand Down Expand Up @@ -190,6 +194,7 @@ impl Capability {
Self::Episodic => "episodic",
Self::SourceSync => "source_sync",
Self::CodingSessions => "coding_sessions",
Self::Scoring => "scoring",
}
}

Expand Down Expand Up @@ -239,6 +244,7 @@ impl Capability {
Self::Episodic => 17,
Self::SourceSync => 18,
Self::CodingSessions => 19,
Self::Scoring => 20,
}
}

Expand Down
9 changes: 5 additions & 4 deletions crates/tinymemory-bus/src/capabilities_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//!
//! Three properties are load-bearing and each has its own test:
//!
//! 1. the enum has exactly the twenty contract families and no more;
//! 1. the enum has exactly the twenty-one contract families and no more;
//! 2. the serialized form is stable snake_case **strings**, never discriminant
//! integers — a driver deployed against an older build must keep advertising
//! the same set after a variant is inserted mid-enum;
Expand All @@ -19,9 +19,9 @@ use super::*;
use serde_json::json;

#[test]
fn capability_has_exactly_the_twenty_contract_families() {
assert_eq!(Capability::ALL.len(), 20);
assert_eq!(Capability::all().len(), 20);
fn capability_has_exactly_the_twenty_one_contract_families() {
assert_eq!(Capability::ALL.len(), 21);
assert_eq!(Capability::all().len(), 21);

let names: Vec<&str> = Capability::ALL.iter().map(|c| c.as_str()).collect();
assert_eq!(
Expand All @@ -47,6 +47,7 @@ fn capability_has_exactly_the_twenty_contract_families() {
"episodic",
"source_sync",
"coding_sessions",
"scoring",
]
);
}
Expand Down
13 changes: 12 additions & 1 deletion crates/tinymemory-bus/src/names.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,14 +316,22 @@ pub mod methods {
pub const CODING_SESSION_STATUS: &str = "CodingSessionStatus";
/// `IngestCodingSessions` — distil coding sessions into observations.
pub const INGEST_CODING_SESSIONS: &str = "IngestCodingSessions";

// Scoring family — entity extraction and text embedding through the bus.
/// `ExtractEntities` — extract canonical entity ids from a query string.
pub const EXTRACT_ENTITIES: &str = "ExtractEntities";
/// `EmbedText` — produce a dense embedding vector for an arbitrary string.
pub const EMBED_TEXT: &str = "EmbedText";
/// `EmbedderSlug` — the stable identifier of the active embedder.
pub const EMBEDDER_SLUG: &str = "EmbedderSlug";
}

/// Every member name, in the order the module declares them.
///
/// The order matters: `tinybus`'s `Interface::members()` returns declaration
/// order, and the module compares the two sequences directly rather than as
/// sets, so a reordering is caught alongside an addition or a removal.
pub const METHODS: [&str; 123] = [
pub const METHODS: [&str; 126] = [
methods::DRIVER_ID,
methods::CAPABILITIES,
methods::HEALTH,
Expand Down Expand Up @@ -447,6 +455,9 @@ pub const METHODS: [&str; 123] = [
methods::REBUILD_FROM_RAW_ARCHIVE,
methods::CODING_SESSION_STATUS,
methods::INGEST_CODING_SESSIONS,
methods::EXTRACT_ENTITIES,
methods::EMBED_TEXT,
methods::EMBEDDER_SLUG,
];

#[cfg(test)]
Expand Down
4 changes: 4 additions & 0 deletions crates/tinymemory-module/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,10 @@ mod exports {
// Local coding-agent transcripts.
"CodingSessionStatus",
"IngestCodingSessions",
// Scoring: entity extraction, text embedding, embedder identification.
"ExtractEntities",
"EmbedText",
"EmbedderSlug",
],
signals = [],
// The host's embedder is deliberately NOT declared as `requires`. That
Expand Down
25 changes: 25 additions & 0 deletions crates/tinymemory-module/src/service/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@
//!
//! CodingSessionStatus() -> [CodingSessionSource]
//! IngestCodingSessions(request) -> CodingSessionIngestReport
//!
//! ExtractEntities(query) -> [String]
//! EmbedText(text) -> [f32]
//! EmbedderSlug() -> String
//! ```
//!
//! # Source scope crosses as an argument, never as ambient state
Expand Down Expand Up @@ -1896,6 +1900,27 @@ impl MemoryService {
.await
.map_err(|error| into_bus_error(&error))
}

async fn extract_entities(&self, query: String) -> BusResult<Vec<String>> {
require_family!(self, as_scoring, Capability::Scoring)
.extract_entities(&query)
.await
.map_err(|error| into_bus_error(&error))
}

async fn embed_text(&self, text: String) -> BusResult<Vec<f32>> {
require_family!(self, as_scoring, Capability::Scoring)
.embed_text(&text)
.await
.map_err(|error| into_bus_error(&error))
}

async fn embedder_slug(&self) -> BusResult<String> {
require_family!(self, as_scoring, Capability::Scoring)
.embedder_slug()
.await
.map_err(|error| into_bus_error(&error))
}
}

/// The response-size ceiling for a method that returns a list of entries.
Expand Down
4 changes: 4 additions & 0 deletions crates/tinymemory-module/tests/module_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,10 @@ const EXPECTED_METHODS: &[&str] = &[
"RebuildFromRawArchive",
"CodingSessionStatus",
"IngestCodingSessions",
// Scoring family.
"ExtractEntities",
"EmbedText",
"EmbedderSlug",
];

#[tokio::test]
Expand Down
53 changes: 48 additions & 5 deletions crates/tinymemory-tinycortex/src/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ use tinymemory_api::provider::{
FacetType, FastRetrieveQuery, MemoryChunks, MemoryCodingSessions, MemoryCore, MemoryDiff,
MemoryDocuments, MemoryEntities, MemoryEpisodic, MemoryGoals, MemoryGraph, MemoryIngest,
MemoryMaintenance, MemoryPeople, MemoryPortability, MemoryProfile, MemoryProvider,
MemoryRecall, MemoryRetrieval, MemorySourceSink, MemorySourceSync, MemoryToolMemory,
MemoryTree, PersonHandle, PersonInteraction, PersonRecord, PersonScore, ProfileFacet,
RankedPerson, RawArchiveCoverage, RawRebuildOutcome, ResolvedPerson, RetrievalHit,
RetrievalResponse, SourceRetrievalQuery, SourceSyncState, SourceSyncStatus, SourceTotal,
SyncAuditEntry, SyncFreshness, SyncRunOutcome, UserState,
MemoryRecall, MemoryRetrieval, MemoryScoring, MemorySourceSink, MemorySourceSync,
MemoryToolMemory, MemoryTree, PersonHandle, PersonInteraction, PersonRecord, PersonScore,
ProfileFacet, RankedPerson, RawArchiveCoverage, RawRebuildOutcome, ResolvedPerson,
RetrievalHit, RetrievalResponse, SourceRetrievalQuery, SourceSyncState, SourceSyncStatus,
SourceTotal, SyncAuditEntry, SyncFreshness, SyncRunOutcome, UserState,
};
use tinymemory_api::recall::OwnedRecallOpts;
use tinymemory_api::tool_memory::ToolMemoryRule;
Expand Down Expand Up @@ -2343,6 +2343,9 @@ impl MemoryProvider for TinycortexProvider {
fn as_coding_sessions(&self) -> Option<&dyn MemoryCodingSessions> {
Some(self)
}
fn as_scoring(&self) -> Option<&dyn MemoryScoring> {
Some(self)
}
}

// ── Source sync ──────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -2786,6 +2789,46 @@ impl MemoryCodingSessions for TinycortexProvider {
}
}

// ── Scoring ──────────────────────────────────────────────────────────────────

#[async_trait]
impl MemoryScoring for TinycortexProvider {
async fn extract_entities(&self, query: &str) -> Result<Vec<String>, MemoryError> {
let config = self.config.clone();
let query = query.to_owned();
let entities = tokio::task::spawn_blocking(move || {
tokio::runtime::Handle::current().block_on(
tinymemory_core::tree::nlp::extract_query_entities(&config, &query),
)
})
.await
.map_err(|error| Self::other("extract entities", error))?;
Ok(entities.into_iter().map(|e| e.canonical_id).collect())
}

async fn embed_text(&self, text: &str) -> Result<Vec<f32>, MemoryError> {
let config = self.config.clone();
let text = text.to_owned();
tokio::task::spawn_blocking(move || {
let embedder =
tinymemory_core::tree::score::embed::factory::build_embedder_from_config(&config)
.map_err(|error| MemoryError::Other(anyhow::anyhow!("{error}")))?;
tokio::runtime::Handle::current()
.block_on(embedder.embed(&text))
.map_err(|error| MemoryError::Other(anyhow::anyhow!("{error}")))
})
.await
.map_err(|error| Self::other("embed text", error))?
}

async fn embedder_slug(&self) -> Result<String, MemoryError> {
Ok(
tinymemory_core::tree::score::embed::factory::effective_embedder_slug(&self.config)
.to_string(),
)
}
}

// ── People ───────────────────────────────────────────────────────────────────
//
// The conversions below destructure both sides exhaustively rather than
Expand Down
Loading