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:
@ -827,6 +827,183 @@ impl From<FirstRunStateOutput> for FirstRunStateDto {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Embedder profiles & engines (LOT C2 — §14.5.3)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
use application::{
|
||||
DismissChoice, DismissEmbedderSuggestionInput, EmbedderEnginesView, ListEmbedderProfilesOutput,
|
||||
OnnxModelView, SaveEmbedderProfileInput, SaveEmbedderProfileOutput,
|
||||
};
|
||||
use domain::profile::EmbedderProfile;
|
||||
|
||||
/// An embedder profile crossing the wire. [`EmbedderProfile`] already serialises
|
||||
/// camelCase (`id, name, strategy, model?, endpoint?, apiKeyEnv?, dimension`), so we
|
||||
/// embed it directly — the TS mirror matches this shape.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct EmbedderProfileDto(pub EmbedderProfile);
|
||||
|
||||
/// A list of embedder profiles (camelCase array on the wire).
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct EmbedderProfileListDto(pub Vec<EmbedderProfileDto>);
|
||||
|
||||
impl From<Vec<EmbedderProfile>> for EmbedderProfileListDto {
|
||||
fn from(v: Vec<EmbedderProfile>) -> Self {
|
||||
Self(v.into_iter().map(EmbedderProfileDto).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ListEmbedderProfilesOutput> for EmbedderProfileListDto {
|
||||
fn from(out: ListEmbedderProfilesOutput) -> Self {
|
||||
out.profiles.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SaveEmbedderProfileOutput> for EmbedderProfileDto {
|
||||
fn from(out: SaveEmbedderProfileOutput) -> Self {
|
||||
Self(out.profile)
|
||||
}
|
||||
}
|
||||
|
||||
/// Request DTO for `save_embedder_profile`: the embedder profile to upsert.
|
||||
///
|
||||
/// Carries the [`EmbedderProfile`] fields directly (the entity validates them in the
|
||||
/// use case). Deserialised camelCase to match the persisted/domain shape.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SaveEmbedderProfileRequestDto {
|
||||
/// The embedder profile to upsert.
|
||||
pub profile: EmbedderProfile,
|
||||
}
|
||||
|
||||
impl From<SaveEmbedderProfileRequestDto> for SaveEmbedderProfileInput {
|
||||
fn from(dto: SaveEmbedderProfileRequestDto) -> Self {
|
||||
let EmbedderProfile {
|
||||
id,
|
||||
name,
|
||||
strategy,
|
||||
model,
|
||||
endpoint,
|
||||
api_key_env,
|
||||
dimension,
|
||||
} = dto.profile;
|
||||
Self {
|
||||
id,
|
||||
name,
|
||||
strategy,
|
||||
model,
|
||||
endpoint,
|
||||
api_key_env,
|
||||
dimension,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One recommendable local ONNX model on the wire (camelCase).
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OnnxModelInfoDto {
|
||||
/// Stable model id accepted by a `localOnnx` profile's `model` field.
|
||||
pub id: String,
|
||||
/// Human-readable name for the UI.
|
||||
pub display_name: String,
|
||||
/// Length of the vectors this model produces.
|
||||
pub dimension: usize,
|
||||
/// Approximate download/disk size in megabytes.
|
||||
pub approx_size_mb: u32,
|
||||
/// Whether this is the recommended default model.
|
||||
pub recommended: bool,
|
||||
}
|
||||
|
||||
impl From<OnnxModelView> for OnnxModelInfoDto {
|
||||
fn from(m: OnnxModelView) -> Self {
|
||||
Self {
|
||||
id: m.id,
|
||||
display_name: m.display_name,
|
||||
dimension: m.dimension,
|
||||
approx_size_mb: m.approx_size_mb,
|
||||
recommended: m.recommended,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Response DTO for `describe_embedder_engines` (drives the "configure an embedder?"
|
||||
/// UI). All-camelCase wire shape.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EmbedderEnginesDto {
|
||||
/// The curated catalogue of recommendable local ONNX models.
|
||||
pub recommended_onnx: Vec<OnnxModelInfoDto>,
|
||||
/// Whether an Ollama-style local embedding server was detected (best-effort).
|
||||
pub ollama_detected: bool,
|
||||
/// Ids of the recommended ONNX models already present in the local cache.
|
||||
pub onnx_cached_models: Vec<String>,
|
||||
/// Whether the HTTP capability (`localServer`/`api`) is compiled into this binary.
|
||||
pub vector_http_enabled: bool,
|
||||
/// Whether the in-process ONNX capability (`localOnnx`) is compiled into this binary.
|
||||
pub vector_onnx_enabled: bool,
|
||||
}
|
||||
|
||||
impl From<EmbedderEnginesView> for EmbedderEnginesDto {
|
||||
fn from(v: EmbedderEnginesView) -> Self {
|
||||
Self {
|
||||
recommended_onnx: v.recommended_onnx.into_iter().map(Into::into).collect(),
|
||||
ollama_detected: v.ollama_detected,
|
||||
onnx_cached_models: v.onnx_cached_models,
|
||||
vector_http_enabled: v.vector_http_enabled,
|
||||
vector_onnx_enabled: v.vector_onnx_enabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Embedder suggestion (LOT C3 — §14.5.5)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// The user's response to the embedder suggestion, on the wire (camelCase:
|
||||
/// `"later"` | `"never"`).
|
||||
#[derive(Debug, Clone, Copy, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum DismissChoiceDto {
|
||||
/// "Plus tard" — re-proposable next session.
|
||||
Later,
|
||||
/// "Ne plus demander" — never again.
|
||||
Never,
|
||||
}
|
||||
|
||||
impl From<DismissChoiceDto> for DismissChoice {
|
||||
fn from(c: DismissChoiceDto) -> Self {
|
||||
match c {
|
||||
DismissChoiceDto::Later => Self::Later,
|
||||
DismissChoiceDto::Never => Self::Never,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Request DTO for `dismiss_embedder_suggestion`. The `project_id` is resolved to a
|
||||
/// project root by the command; `choice` is the user's dismissal.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DismissEmbedderSuggestionRequestDto {
|
||||
/// The project the suggestion concerned (UUID string).
|
||||
pub project_id: String,
|
||||
/// The user's choice.
|
||||
pub choice: DismissChoiceDto,
|
||||
}
|
||||
|
||||
impl DismissEmbedderSuggestionRequestDto {
|
||||
/// Builds the use-case input from a resolved project root + the DTO choice.
|
||||
#[must_use]
|
||||
pub fn into_input(self, project_root: domain::ProjectPath) -> DismissEmbedderSuggestionInput {
|
||||
DismissEmbedderSuggestionInput {
|
||||
project_root,
|
||||
choice: self.choice.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a [`DeleteProfileInput`] from a raw profile-id string.
|
||||
///
|
||||
/// # Errors
|
||||
@ -927,6 +1104,16 @@ pub struct UpdateAgentContextRequestDto {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
/// Request DTO for `update_project_context`.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateProjectContextRequestDto {
|
||||
/// Id of the owning project.
|
||||
pub project_id: String,
|
||||
/// New project-level Markdown context, stored under `.ideai/CONTEXT.md`.
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
/// Request DTO for `launch_agent`.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@ -1015,6 +1202,9 @@ pub struct LiveAgentDto {
|
||||
pub agent_id: String,
|
||||
/// The hosting layout leaf's node id (UUID string).
|
||||
pub node_id: String,
|
||||
/// The live PTY session id, used to reattach a newly-opened cell without
|
||||
/// respawning the agent.
|
||||
pub session_id: String,
|
||||
}
|
||||
|
||||
/// Response DTO for `list_live_agents` (transparent array on the wire).
|
||||
@ -1023,21 +1213,35 @@ pub struct LiveAgentDto {
|
||||
pub struct LiveAgentListDto(pub Vec<LiveAgentDto>);
|
||||
|
||||
impl LiveAgentListDto {
|
||||
/// Builds the wire list from the registry's `(AgentId, NodeId)` pairs.
|
||||
/// Builds the wire list from the registry's `(AgentId, NodeId, SessionId)` tuples.
|
||||
#[must_use]
|
||||
pub fn from_pairs(pairs: Vec<(AgentId, NodeId)>) -> Self {
|
||||
pub fn from_pairs(pairs: Vec<(AgentId, NodeId, domain::SessionId)>) -> Self {
|
||||
Self(
|
||||
pairs
|
||||
.into_iter()
|
||||
.map(|(agent_id, node_id)| LiveAgentDto {
|
||||
.map(|(agent_id, node_id, session_id)| LiveAgentDto {
|
||||
agent_id: agent_id.to_string(),
|
||||
node_id: node_id.to_string(),
|
||||
session_id: session_id.to_string(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Request DTO for `attach_live_agent`: bind an already-running agent session to
|
||||
/// a visible layout cell without spawning a new process.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AttachLiveAgentRequestDto {
|
||||
/// Id of the owning project (validated for symmetry and future scoping).
|
||||
pub project_id: String,
|
||||
/// Id of the already-running agent.
|
||||
pub agent_id: String,
|
||||
/// Layout leaf that should display the live session.
|
||||
pub node_id: String,
|
||||
}
|
||||
|
||||
/// Parses an agent-id string (UUID) coming from the frontend.
|
||||
///
|
||||
/// # Errors
|
||||
@ -1056,9 +1260,9 @@ pub fn parse_agent_id(raw: &str) -> Result<AgentId, ErrorDto> {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
use application::{
|
||||
AgentDrift, CreateAgentFromTemplateInput, CreateTemplateInput,
|
||||
CreateTemplateOutput, DetectAgentDriftOutput, ListTemplatesOutput, SyncAgentWithTemplateOutput,
|
||||
UpdateTemplateInput, UpdateTemplateOutput,
|
||||
AgentDrift, CreateAgentFromTemplateInput, CreateTemplateInput, CreateTemplateOutput,
|
||||
DetectAgentDriftOutput, ListTemplatesOutput, SyncAgentWithTemplateOutput, UpdateTemplateInput,
|
||||
UpdateTemplateOutput,
|
||||
};
|
||||
use domain::{AgentTemplate, TemplateId};
|
||||
|
||||
@ -1253,9 +1457,7 @@ pub struct SyncAgentWithTemplateRequestDto {
|
||||
// Git (L8)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
use application::{
|
||||
GitBranchesOutput, GitCommitOutput, GitLogOutput, GitStatusOutput,
|
||||
};
|
||||
use application::{GitBranchesOutput, GitCommitOutput, GitLogOutput, GitStatusOutput};
|
||||
use domain::ports::{GitCommitInfo, GitFileStatus, GraphCommit};
|
||||
|
||||
/// One changed path returned by `git_status`.
|
||||
@ -1284,7 +1486,12 @@ pub struct GitStatusListDto(pub Vec<GitFileStatusDto>);
|
||||
|
||||
impl From<GitStatusOutput> for GitStatusListDto {
|
||||
fn from(out: GitStatusOutput) -> Self {
|
||||
Self(out.entries.into_iter().map(GitFileStatusDto::from).collect())
|
||||
Self(
|
||||
out.entries
|
||||
.into_iter()
|
||||
.map(GitFileStatusDto::from)
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1655,13 +1862,23 @@ pub struct MemoryIndexDto(pub Vec<MemoryIndexEntryDto>);
|
||||
|
||||
impl From<ReadMemoryIndexOutput> for MemoryIndexDto {
|
||||
fn from(out: ReadMemoryIndexOutput) -> Self {
|
||||
Self(out.entries.into_iter().map(MemoryIndexEntryDto::from).collect())
|
||||
Self(
|
||||
out.entries
|
||||
.into_iter()
|
||||
.map(MemoryIndexEntryDto::from)
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RecallMemoryOutput> for MemoryIndexDto {
|
||||
fn from(out: RecallMemoryOutput) -> Self {
|
||||
Self(out.entries.into_iter().map(MemoryIndexEntryDto::from).collect())
|
||||
Self(
|
||||
out.entries
|
||||
.into_iter()
|
||||
.map(MemoryIndexEntryDto::from)
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user