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

@ -57,9 +57,7 @@ async fn drain_request(stream: &mut tokio::net::TcpStream) {
}
fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack
.windows(needle.len())
.position(|w| w == needle)
haystack.windows(needle.len()).position(|w| w == needle)
}
/// Builds an HTTP `200 OK` response with a JSON body and the right `Content-Length`.
@ -98,17 +96,22 @@ async fn http_embedder_parses_vectors_and_restores_input_order() {
.embed(&["first".to_string(), "second".to_string()])
.await
.expect("embed must succeed");
assert_eq!(out, vec![vec![1.0, 0.0], vec![0.0, 1.0]], "input order restored by index");
assert_eq!(
out,
vec![vec![1.0, 0.0], vec![0.0, 1.0]],
"input order restored by index"
);
}
#[tokio::test]
async fn http_embedder_empty_input_short_circuits_without_network() {
// A closed/never-bound endpoint: no request must be made for empty input.
let embedder = HttpEmbedder::from_profile(&local_server_profile(
"http://127.0.0.1:1/v1/embeddings",
4,
));
let out = embedder.embed(&[]).await.expect("empty input ⇒ empty output, no I/O");
let embedder =
HttpEmbedder::from_profile(&local_server_profile("http://127.0.0.1:1/v1/embeddings", 4));
let out = embedder
.embed(&[])
.await
.expect("empty input ⇒ empty output, no I/O");
assert!(out.is_empty());
}
@ -124,10 +127,8 @@ async fn http_embedder_non_2xx_is_unavailable() {
#[tokio::test]
async fn http_embedder_unreachable_host_is_unavailable() {
// Port 1 on loopback: nothing listens ⇒ connection refused ⇒ Unavailable.
let embedder = HttpEmbedder::from_profile(&local_server_profile(
"http://127.0.0.1:1/v1/embeddings",
2,
));
let embedder =
HttpEmbedder::from_profile(&local_server_profile("http://127.0.0.1:1/v1/embeddings", 2));
let err = embedder.embed(&["x".to_string()]).await.unwrap_err();
assert!(matches!(err, EmbedderError::Unavailable(_)), "got {err:?}");
}