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:
115
crates/application/src/project/context.rs
Normal file
115
crates/application/src/project/context.rs
Normal file
@ -0,0 +1,115 @@
|
||||
//! Project-level context stored under `.ideai/CONTEXT.md`.
|
||||
//!
|
||||
//! This context is model-agnostic and shared by every agent/profile launch. It is
|
||||
//! deliberately project-local: deleting `.ideai/` removes the context together
|
||||
//! with every other IdeA artefact for that project.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::ports::{FileSystem, FsError, RemotePath};
|
||||
use domain::Project;
|
||||
|
||||
use crate::error::AppError;
|
||||
|
||||
use super::meta::{join_root, IDEAI_DIR};
|
||||
|
||||
/// Project-context file name inside `.ideai/`.
|
||||
pub const PROJECT_CONTEXT_FILE: &str = "CONTEXT.md";
|
||||
|
||||
/// Input for [`ReadProjectContext::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ReadProjectContextInput {
|
||||
/// Project whose `.ideai/CONTEXT.md` should be read.
|
||||
pub project: Project,
|
||||
}
|
||||
|
||||
/// Output of [`ReadProjectContext::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ReadProjectContextOutput {
|
||||
/// Markdown content. Empty when the file does not exist yet.
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
/// Reads `.ideai/CONTEXT.md` tolerantly.
|
||||
pub struct ReadProjectContext {
|
||||
fs: Arc<dyn FileSystem>,
|
||||
}
|
||||
|
||||
impl ReadProjectContext {
|
||||
/// Builds the use case.
|
||||
#[must_use]
|
||||
pub fn new(fs: Arc<dyn FileSystem>) -> Self {
|
||||
Self { fs }
|
||||
}
|
||||
|
||||
/// Executes the read.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`AppError::FileSystem`] on non-missing filesystem failures, and
|
||||
/// [`AppError::Store`] if the file is not valid UTF-8.
|
||||
pub async fn execute(
|
||||
&self,
|
||||
input: ReadProjectContextInput,
|
||||
) -> Result<ReadProjectContextOutput, AppError> {
|
||||
match self.fs.read(&project_context_path(&input.project)).await {
|
||||
Ok(bytes) => {
|
||||
let content = String::from_utf8(bytes)
|
||||
.map_err(|e| AppError::Store(format!("project context is not UTF-8: {e}")))?;
|
||||
Ok(ReadProjectContextOutput { content })
|
||||
}
|
||||
Err(FsError::NotFound(_)) => Ok(ReadProjectContextOutput {
|
||||
content: String::new(),
|
||||
}),
|
||||
Err(e) => Err(AppError::FileSystem(e.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Input for [`UpdateProjectContext::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct UpdateProjectContextInput {
|
||||
/// Project whose `.ideai/CONTEXT.md` should be overwritten.
|
||||
pub project: Project,
|
||||
/// New Markdown content.
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
/// Overwrites `.ideai/CONTEXT.md`.
|
||||
pub struct UpdateProjectContext {
|
||||
fs: Arc<dyn FileSystem>,
|
||||
}
|
||||
|
||||
impl UpdateProjectContext {
|
||||
/// Builds the use case.
|
||||
#[must_use]
|
||||
pub fn new(fs: Arc<dyn FileSystem>) -> Self {
|
||||
Self { fs }
|
||||
}
|
||||
|
||||
/// Executes the update.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`AppError::FileSystem`] if `.ideai/` cannot be created or the
|
||||
/// context file cannot be written.
|
||||
pub async fn execute(&self, input: UpdateProjectContextInput) -> Result<(), AppError> {
|
||||
self.fs
|
||||
.create_dir_all(&RemotePath::new(join_root(&input.project.root, IDEAI_DIR)))
|
||||
.await?;
|
||||
self.fs
|
||||
.write(
|
||||
&project_context_path(&input.project),
|
||||
input.content.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Absolute path to `<root>/.ideai/CONTEXT.md`.
|
||||
#[must_use]
|
||||
pub fn project_context_path(project: &Project) -> RemotePath {
|
||||
RemotePath::new(join_root(
|
||||
&project.root,
|
||||
&format!("{IDEAI_DIR}/{PROJECT_CONTEXT_FILE}"),
|
||||
))
|
||||
}
|
||||
@ -14,11 +14,18 @@
|
||||
//! - [`ListProjects`] — list known projects from the registry.
|
||||
|
||||
mod close;
|
||||
mod context;
|
||||
mod create;
|
||||
pub(crate) mod meta;
|
||||
mod open;
|
||||
|
||||
pub use close::{CloseProject, CloseProjectInput, CloseProjectOutput, CloseTab, CloseTabInput};
|
||||
pub use context::{
|
||||
project_context_path, ReadProjectContext, ReadProjectContextInput, ReadProjectContextOutput,
|
||||
UpdateProjectContext, UpdateProjectContextInput, PROJECT_CONTEXT_FILE,
|
||||
};
|
||||
pub use create::{CreateProject, CreateProjectInput, CreateProjectOutput};
|
||||
pub use meta::ProjectMeta;
|
||||
pub use open::{ListProjects, ListProjectsOutput, OpenProject, OpenProjectInput, OpenProjectOutput};
|
||||
pub use open::{
|
||||
ListProjects, ListProjectsOutput, OpenProject, OpenProjectInput, OpenProjectOutput,
|
||||
};
|
||||
|
||||
@ -7,9 +7,7 @@ use domain::{AgentManifest, Project, ProjectId};
|
||||
|
||||
use crate::error::AppError;
|
||||
|
||||
use super::meta::{
|
||||
from_json_bytes, join_root, ProjectMeta, AGENTS_FILE, IDEAI_DIR, PROJECT_FILE,
|
||||
};
|
||||
use super::meta::{from_json_bytes, join_root, ProjectMeta, AGENTS_FILE, IDEAI_DIR, PROJECT_FILE};
|
||||
|
||||
/// Input for [`OpenProject::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
||||
Reference in New Issue
Block a user