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

@ -16,7 +16,7 @@
use std::sync::Arc;
use domain::ports::ProfileStore;
use domain::{OrchestratorCommand, Project, ProfileId};
use domain::{OrchestratorCommand, OrchestratorVisibility, ProfileId, Project};
use crate::agent::{
CreateAgentFromScratch, CreateAgentInput, LaunchAgent, LaunchAgentInput, ListAgents,
@ -95,7 +95,11 @@ impl OrchestratorService {
name,
profile,
context,
} => self.spawn_agent(project, name, profile, context).await,
visibility,
} => {
self.spawn_agent(project, name, profile, context, visibility)
.await
}
OrchestratorCommand::StopAgent { name } => self.stop_agent(project, name).await,
OrchestratorCommand::UpdateAgentContext { name, context } => {
self.update_agent_context(project, name, context).await
@ -115,15 +119,19 @@ impl OrchestratorService {
&self,
project: &Project,
name: String,
profile: String,
profile: Option<String>,
context: Option<String>,
visibility: OrchestratorVisibility,
) -> Result<OrchestratorOutcome, AppError> {
let existing = self.find_agent_id_by_name(project, &name).await?;
let agent_id = match existing {
Some(id) => id,
None => {
let profile_id = self.resolve_profile(&profile).await?;
let profile = profile.as_deref().ok_or_else(|| {
AppError::Invalid("profile is required to create an agent".to_owned())
})?;
let profile_id = self.resolve_profile(profile).await?;
let created = self
.create_agent
.execute(CreateAgentInput {
@ -137,19 +145,54 @@ impl OrchestratorService {
}
};
if let Some(session_id) = self.sessions.session_for_agent(&agent_id) {
match visibility {
OrchestratorVisibility::Background => {
return Ok(OrchestratorOutcome {
detail: format!("agent {name} already running in background"),
});
}
OrchestratorVisibility::Visible { node_id } => {
let session = self
.sessions
.rebind_agent_node(&agent_id, node_id)
.ok_or_else(|| {
AppError::NotFound(format!(
"running session {session_id} for agent {name}"
))
})?;
return Ok(OrchestratorOutcome {
detail: format!("attached agent {name} to cell {}", session.node_id),
});
}
}
}
let node_id = match visibility {
OrchestratorVisibility::Background => None,
OrchestratorVisibility::Visible { node_id } => Some(node_id),
};
self.launch_agent
.execute(LaunchAgentInput {
project: project.clone(),
agent_id,
rows: DEFAULT_ROWS,
cols: DEFAULT_COLS,
node_id: None,
node_id,
conversation_id: None,
})
.await?;
Ok(OrchestratorOutcome {
detail: format!("launched agent {name}"),
detail: match visibility {
OrchestratorVisibility::Background => {
format!("launched agent {name} in background")
}
OrchestratorVisibility::Visible { node_id } => {
format!("launched agent {name} in cell {node_id}")
}
},
})
}