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

@ -71,7 +71,8 @@ async fn onnx_unknown_model_is_unsupported() {
// *before* any `try_new`/download — so this is safe without a network. We point
// the cache at a path that is never created to prove no download is attempted.
let cache = std::path::Path::new("/idea-onnx-cache-that-never-exists");
let embedder = OnnxEmbedder::from_profile(&onnx_profile(Some("definitely-unknown"), 384), cache);
let embedder =
OnnxEmbedder::from_profile(&onnx_profile(Some("definitely-unknown"), 384), cache);
let err = embedder
.embed(&["x".to_string()])
@ -113,7 +114,8 @@ async fn onnx_construction_is_cheap() {
// and an unknown one (construction never fails for either).
let cache = std::path::Path::new("/idea-onnx-cache-that-never-exists-3");
let known = OnnxEmbedder::from_profile(&onnx_profile(Some("multilingual-e5-small"), 384), cache);
let known =
OnnxEmbedder::from_profile(&onnx_profile(Some("multilingual-e5-small"), 384), cache);
assert_eq!(known.id(), "test-onnx");
assert_eq!(known.dimension(), 384);
@ -139,14 +141,16 @@ async fn embedder_from_profile_localonnx_is_real_engine_not_unsupported_stub() {
// whereas the StubEmbedder would return Unsupported even for an empty batch.
let cache = std::path::Path::new("/idea-onnx-cache-that-never-exists-4");
let known = onnx_profile(Some("multilingual-e5-small"), 384);
let embedder =
embedder_from_profile(&known, cache).expect("localOnnx must yield an embedder");
let embedder = embedder_from_profile(&known, cache).expect("localOnnx must yield an embedder");
assert_eq!(embedder.dimension(), 384);
let out = embedder
.embed(&[])
.await
.expect("real engine short-circuits empty input to Ok(vec![])");
assert!(out.is_empty(), "empty batch ⇒ empty result on the real engine");
assert!(
out.is_empty(),
"empty batch ⇒ empty result on the real engine"
);
// And an *unknown* model still surfaces Unsupported (deferred resolution), so the
// mapping is the real engine in both cases (the stub would also say Unsupported,
@ -187,11 +191,17 @@ async fn onnx_embeds_e5_small_real_model() {
for v in &out {
assert_eq!(v.len(), 384, "e5-small produces 384-dim vectors");
let norm = v.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!((norm - 1.0).abs() < 1e-2, "fastembed L2-normalises; norm ≈ 1, got {norm}");
assert!(
(norm - 1.0).abs() < 1e-2,
"fastembed L2-normalises; norm ≈ 1, got {norm}"
);
}
// Deterministic across calls (model already cached after the first call).
let again = embedder.embed(&texts).await.expect("second embedding must succeed");
let again = embedder
.embed(&texts)
.await
.expect("second embedding must succeed");
assert_eq!(out, again, "embedding must be deterministic across calls");
}