feat(memory): système de mémoire projet model-agnostic (L14, LOT A+B+C)
Base de connaissance persistante par projet, indépendante de tout modèle/CLU
et de git. Cadrage archi en §14.5 (ARCHITECTURE.md), cycle Archi→Dev→Test.
LOT A — étage 1 (.md, source de vérité)
- domaine: entité Memory (+ MemorySlug, MemoryType, MemoryFrontmatter,
MemoryLink, MemoryIndexEntry), liens [[slug]], index MEMORY.md dérivé
- port MemoryStore + MemoryError, adapter FsMemoryStore (.ideai/memory/)
- application: 7 use cases (Create/Update/List/Get/Delete/ReadIndex/
ResolveLinks), From<MemoryError> for AppError
- app-tauri: commandes + DTO, events MemorySaved/MemoryDeleted
- suppression de la variante morte DomainError::MalformedFrontmatter
LOT B — rappel adaptatif (étage 1)
- port MemoryRecall + MemoryQuery, adapter NaiveMemoryRecall (troncature
au budget de tokens, court-circuit budget-0), use case RecallMemory
LOT C — étage 2 vectoriel (structure complète, zéro dépendance lourde)
- port Embedder + EmbedderError, profils déclaratifs EmbedderProfile/
EmbedderStrategy (embedder.json)
- VectorMemoryRecall (cosinus, cache .ideai/memory/.index/ gitignoré)
- AdaptiveMemoryRecall (bascule pure should_use_vector), défaut none
- HashEmbedder (déterministe, tests), StubEmbedder (onnx/server/api)
Tests: 57 binaires verts, build + clippy --workspace sans warning.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -150,6 +150,107 @@ pub struct AgentProfile {
|
||||
pub session: Option<SessionStrategy>,
|
||||
}
|
||||
|
||||
/// Embedding strategy of an [`EmbedderProfile`] (LOT C, étage 2 vectoriel).
|
||||
///
|
||||
/// Declarative, Open/Closed: a new engine family is a new variant *only* if it
|
||||
/// changes the adapter's dispatch; otherwise it is pure data on the profile
|
||||
/// (`model`, `endpoint`, …). The default product posture is [`None`](Self::None):
|
||||
/// no embedding ⇒ no heavy dependency, recall stays the naïve étage 1.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum EmbedderStrategy {
|
||||
/// A local ONNX model run in-process (real integration is a follow-up).
|
||||
LocalOnnx,
|
||||
/// A local embedding server reached over HTTP (real integration is a follow-up).
|
||||
LocalServer,
|
||||
/// A remote embedding API (real integration is a follow-up).
|
||||
Api,
|
||||
/// No embedding engine: recall stays the dependency-free naïve étage 1.
|
||||
None,
|
||||
}
|
||||
|
||||
/// Declarative configuration for one embedding engine (LOT C, étage 2).
|
||||
///
|
||||
/// Stored in the global IDE store as `embedder.json`, mirroring `profiles.json`
|
||||
/// for [`AgentProfile`]s: adding an engine is **data, not code** (Open/Closed).
|
||||
///
|
||||
/// Invariants:
|
||||
/// - `id` and `name` non-empty,
|
||||
/// - `dimension` is non-zero (an embedder always produces fixed-length vectors).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EmbedderProfile {
|
||||
/// Stable identifier (e.g. `"local-onnx-minilm"`).
|
||||
pub id: String,
|
||||
/// Display name.
|
||||
pub name: String,
|
||||
/// Embedding strategy driving which concrete adapter is used.
|
||||
pub strategy: EmbedderStrategy,
|
||||
/// Model identifier (e.g. an ONNX model name), when the strategy needs one.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub model: Option<String>,
|
||||
/// Endpoint URL for a server/API strategy.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub endpoint: Option<String>,
|
||||
/// Name of the environment variable carrying the API key (never the key
|
||||
/// itself), for an `api` strategy.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub api_key_env: Option<String>,
|
||||
/// Length of the vectors this engine produces.
|
||||
pub dimension: usize,
|
||||
}
|
||||
|
||||
impl EmbedderProfile {
|
||||
/// Builds a validated embedder profile.
|
||||
///
|
||||
/// # Errors
|
||||
/// - [`DomainError::EmptyField`] if `id` or `name` is empty,
|
||||
/// - [`DomainError::EmptyField`] (`"embedder.dimension"`) if `dimension` is `0`.
|
||||
pub fn new(
|
||||
id: impl Into<String>,
|
||||
name: impl Into<String>,
|
||||
strategy: EmbedderStrategy,
|
||||
model: Option<String>,
|
||||
endpoint: Option<String>,
|
||||
api_key_env: Option<String>,
|
||||
dimension: usize,
|
||||
) -> Result<Self, DomainError> {
|
||||
let id = id.into();
|
||||
let name = name.into();
|
||||
crate::validation::non_empty(&id, "embedder.id")?;
|
||||
crate::validation::non_empty(&name, "embedder.name")?;
|
||||
if dimension == 0 {
|
||||
return Err(DomainError::EmptyField {
|
||||
field: "embedder.dimension",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
id,
|
||||
name,
|
||||
strategy,
|
||||
model,
|
||||
endpoint,
|
||||
api_key_env,
|
||||
dimension,
|
||||
})
|
||||
}
|
||||
|
||||
/// A dependency-free default profile: strategy [`EmbedderStrategy::None`].
|
||||
/// Recall stays the naïve étage 1 — nothing is imposed.
|
||||
#[must_use]
|
||||
pub fn none() -> Self {
|
||||
Self {
|
||||
id: "none".to_owned(),
|
||||
name: "None".to_owned(),
|
||||
strategy: EmbedderStrategy::None,
|
||||
model: None,
|
||||
endpoint: None,
|
||||
api_key_env: None,
|
||||
dimension: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentProfile {
|
||||
/// Builds a validated profile.
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user