feat(memory): config embedders (LOT C2) + suggestion contextuelle (LOT C3) + contexte projet partagé

- LOT C2 (§14.5.3) : use cases de configuration des embedders déclaratifs
  (List/Save/Delete + DescribeEmbedderEngines : modèles ONNX recommandés,
  environnement local détecté, stratégies compilées). UI EmbedderSettings.
- LOT C3 (§14.5.5) : suggestion contextuelle best-effort à l'activation quand la
  mémoire dépasse le budget de recall sans embedder configuré (event
  EmbedderSuggested, anti-spam 1×/session, « ne plus demander »).
- Contexte projet partagé .ideai/CONTEXT.md (model-agnostic) injecté à tous les
  agents/profils au lancement, avant la persona. UI ProjectContextPanel.

Tests : backend workspace vert (0 échec) ; frontend 306/306.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 09:24:51 +02:00
parent 32398827fb
commit 785e9935fd
118 changed files with 5793 additions and 866 deletions

View File

@ -32,7 +32,7 @@ use crate::events::DomainEvent;
use crate::ids::{AgentId, SessionId};
use crate::markdown::MarkdownDoc;
use crate::memory::{Memory, MemoryIndexEntry, MemoryLink, MemorySlug};
use crate::profile::AgentProfile;
use crate::profile::{AgentProfile, EmbedderProfile};
use crate::project::{Project, ProjectPath};
use crate::remote::RemoteKind;
use crate::skill::{Skill, SkillScope};
@ -798,6 +798,94 @@ pub trait ProfileStore: Send + Sync {
async fn mark_configured(&self) -> Result<(), StoreError>;
}
/// CRUD for the declarative [`EmbedderProfile`]s in the global IDE store
/// (`embedder.json`, LOT C, §14.5.3). Mirrors [`ProfileStore`] for the embedding
/// engines: adding an engine is *data* the user configures, not code.
///
/// There is **no** `is_configured`/`mark_configured`: the default embedder posture
/// is `none` **by absence of the file** (an empty/missing `embedder.json` ⇒ recall
/// stays the dependency-free naïve étage 1). A configured embedder takes effect at
/// the **next IDE start** (the composition root freezes the recall for the session).
#[async_trait]
pub trait EmbedderProfileStore: Send + Sync {
/// Lists all configured embedder profiles (empty when none configured).
///
/// # Errors
/// [`StoreError`] on failure.
async fn list(&self) -> Result<Vec<EmbedderProfile>, StoreError>;
/// Saves (creates or replaces by id) an embedder profile.
///
/// # Errors
/// [`StoreError`] on failure.
async fn save(&self, profile: &EmbedderProfile) -> Result<(), StoreError>;
/// Deletes an embedder profile by id.
///
/// # Errors
/// [`StoreError::NotFound`] if no profile carries that id.
async fn delete(&self, id: &str) -> Result<(), StoreError>;
}
/// Best-effort snapshot of the embedding *environment*: which local engines are
/// already installed/cached on this machine. Drives the "configure an embedder?"
/// UI (C2/C3) so it can detect an existing engine before suggesting a download.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct EmbedderEnvReport {
/// Whether an Ollama-style local embedding server was detected (best-effort;
/// always `false` in a build without the HTTP capability).
pub ollama_detected: bool,
/// Ids of the recommended ONNX models already present in the local cache.
pub onnx_cached_models: Vec<String>,
}
/// Probes the local embedding environment (installed/cached engines). **Best-effort
/// by contract**: [`inspect`](Self::inspect) never errors — an unreachable host or
/// an unreadable cache simply yields a report with the corresponding fields unset.
#[async_trait]
pub trait EmbedderEnvInspector: Send + Sync {
/// Returns a best-effort snapshot of the local embedding environment.
async fn inspect(&self) -> EmbedderEnvReport;
}
/// The persisted user response to the "configure an embedder?" suggestion
/// (ARCHITECTURE §14.5.5, LOT C3), stored per project under
/// `.ideai/memory/.embedder-prompt.json`. Absence of the file ⇒ never answered.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EmbedderPromptDismissal {
/// "Plus tard" — re-proposable on a future session (not this one).
Later,
/// "Ne plus demander" — never publish the suggestion again (persistent).
Never,
}
/// Persistence of the per-project embedder-suggestion state
/// (`.ideai/memory/.embedder-prompt.json`, LOT C3). A fine-grained port (Interface
/// Segregation) so the suggestion use cases depend on this alone, not on a wider
/// store. **Best-effort reads**: a missing/malformed file is *not* an error — it
/// reads as "never answered" ([`None`]).
#[async_trait]
pub trait EmbedderPromptStore: Send + Sync {
/// Reads the recorded dismissal for `root`'s project, or `None` when the user
/// has never answered (no file / unreadable / malformed).
///
/// # Errors
/// [`StoreError`] only on an unexpected I/O failure the adapter chooses to
/// surface; a plain missing file degrades to `Ok(None)`.
async fn read(&self, root: &ProjectPath)
-> Result<Option<EmbedderPromptDismissal>, StoreError>;
/// Records the user's dismissal choice for `root`'s project.
///
/// # Errors
/// [`StoreError`] on a persistence failure.
async fn write(
&self,
root: &ProjectPath,
dismissal: EmbedderPromptDismissal,
) -> Result<(), StoreError>;
}
/// Reads/writes agent `.md` contexts and the project manifest, within a project.
#[async_trait]
pub trait AgentContextStore: Send + Sync {
@ -895,8 +983,7 @@ pub trait GitPort: Send + Sync {
///
/// # Errors
/// [`GitError`] on failure.
async fn log(&self, root: &ProjectPath, limit: usize)
-> Result<Vec<GitCommitInfo>, GitError>;
async fn log(&self, root: &ProjectPath, limit: usize) -> Result<Vec<GitCommitInfo>, GitError>;
/// Returns the commit graph (all local branches, topological + time sort).
///