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

@ -11,8 +11,8 @@ use application::AppError;
use application::{
CreateAgentOutput, InspectConversationOutput, LaunchAgentOutput, ListAgentsOutput,
};
use domain::ports::ConversationDetails;
use domain::ids::{AgentId, NodeId, ProfileId, SessionId};
use domain::ports::ConversationDetails;
use domain::terminal::{PtySize, SessionKind, SessionStatus, TerminalSession};
use domain::{Agent, AgentOrigin, ProjectPath};
use serde_json::json;
@ -44,7 +44,10 @@ fn agent_dto_serialises_camelcase() {
assert_eq!(v["id"], agent.id.to_string());
assert_eq!(v["name"], "My Agent");
assert_eq!(v["contextPath"], "agents/my-agent.md", "camelCase key");
assert_eq!(v["profileId"], ProfileId::from_uuid(Uuid::from_u128(2)).to_string());
assert_eq!(
v["profileId"],
ProfileId::from_uuid(Uuid::from_u128(2)).to_string()
);
assert_eq!(v["synchronized"], false);
// origin: tagged `{ "type": "scratch" }`
assert_eq!(v["origin"]["type"], "scratch");
@ -68,7 +71,9 @@ fn agent_list_dto_is_transparent_array() {
#[test]
fn create_agent_output_maps_to_agent_dto() {
let agent = make_agent(5, 6);
let out = CreateAgentOutput { agent: agent.clone() };
let out = CreateAgentOutput {
agent: agent.clone(),
};
let dto = AgentDto::from(out);
assert_eq!(dto.0.id, agent.id);
}
@ -168,19 +173,25 @@ fn agent_already_running_error_code_is_stable() {
// Option A: a stable code + a text message (the node is NOT enriched into the
// ErrorDto — the frontend branches on the code alone).
assert_eq!(dto.code, "AGENT_ALREADY_RUNNING");
assert!(dto.message.contains("already running"), "message: {}", dto.message);
assert!(
dto.message.contains("already running"),
"message: {}",
dto.message
);
}
#[test]
fn live_agent_list_dto_serialises_camelcase_array() {
let agent_a = AgentId::from_uuid(Uuid::from_u128(11));
let node_a = NodeId::from_uuid(Uuid::from_u128(21));
let dto = LiveAgentListDto::from_pairs(vec![(agent_a, node_a)]);
let session_a = domain::SessionId::from_uuid(Uuid::from_u128(31));
let dto = LiveAgentListDto::from_pairs(vec![(agent_a, node_a, session_a)]);
let v = serde_json::to_value(&dto).unwrap();
let arr = v.as_array().expect("transparent array");
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["agentId"], agent_a.to_string());
assert_eq!(arr[0]["nodeId"], node_a.to_string());
assert_eq!(arr[0]["sessionId"], session_a.to_string());
// No snake_case leak.
assert!(arr[0].get("agent_id").is_none());
assert!(arr[0].get("node_id").is_none());
@ -246,7 +257,10 @@ fn conversation_details_dto_omits_fields_when_none() {
let v = serde_json::to_value(&dto).unwrap();
// Both optional fields are omitted from the wire (absent, not null).
assert!(v.get("lastTopic").is_none(), "absent topic ⇒ field omitted");
assert!(v.get("tokenCount").is_none(), "absent tokens ⇒ field omitted");
assert!(
v.get("tokenCount").is_none(),
"absent tokens ⇒ field omitted"
);
assert_eq!(v, json!({}), "fully degraded ⇒ empty object");
}