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:
@ -16,6 +16,7 @@ use std::sync::{Arc, Mutex};
|
||||
use async_trait::async_trait;
|
||||
use domain::agent::{AgentManifest, ManifestEntry};
|
||||
use domain::events::DomainEvent;
|
||||
use domain::ids::SkillId;
|
||||
use domain::ids::{AgentId, ProfileId, ProjectId};
|
||||
use domain::markdown::MarkdownDoc;
|
||||
use domain::ports::{
|
||||
@ -24,7 +25,6 @@ use domain::ports::{
|
||||
PtyError, PtyHandle, PtyPort, RemotePath, RuntimeError, SessionPlan, SkillStore, SpawnSpec,
|
||||
StoreError,
|
||||
};
|
||||
use domain::ids::SkillId;
|
||||
use domain::profile::{AgentProfile, ContextInjection};
|
||||
use domain::project::{Project, ProjectPath};
|
||||
use domain::remote::RemoteRef;
|
||||
@ -94,7 +94,13 @@ impl AgentContextStore for FakeContexts {
|
||||
) -> Result<MarkdownDoc, StoreError> {
|
||||
let md = self.md_path_of(agent).ok_or(StoreError::NotFound)?;
|
||||
Ok(MarkdownDoc::new(
|
||||
self.0.lock().unwrap().contents.get(&md).cloned().unwrap_or_default(),
|
||||
self.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contents
|
||||
.get(&md)
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
))
|
||||
}
|
||||
async fn write_context(
|
||||
@ -150,7 +156,11 @@ impl ProfileStore for FakeProfiles {
|
||||
struct FakeSkills;
|
||||
#[async_trait]
|
||||
impl SkillStore for FakeSkills {
|
||||
async fn list(&self, _scope: SkillScope, _root: &ProjectPath) -> Result<Vec<Skill>, StoreError> {
|
||||
async fn list(
|
||||
&self,
|
||||
_scope: SkillScope,
|
||||
_root: &ProjectPath,
|
||||
) -> Result<Vec<Skill>, StoreError> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
async fn get(
|
||||
@ -321,6 +331,7 @@ fn build_service(contexts: FakeContexts) -> Arc<OrchestratorService> {
|
||||
bus.clone(),
|
||||
Arc::new(SeqIds(Mutex::new(1))),
|
||||
Arc::new(FakeRecall),
|
||||
None,
|
||||
));
|
||||
let list = Arc::new(ListAgents::new(Arc::new(contexts.clone())));
|
||||
let close = Arc::new(CloseTerminal::new(Arc::new(FakePty), Arc::clone(&sessions)));
|
||||
@ -342,7 +353,11 @@ fn build_service(contexts: FakeContexts) -> Arc<OrchestratorService> {
|
||||
}
|
||||
|
||||
fn read_response(request_path: &std::path::Path) -> OrchestratorResponse {
|
||||
let mut name = request_path.file_name().unwrap().to_string_lossy().into_owned();
|
||||
let mut name = request_path
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
name.push_str(".response.json");
|
||||
let response_path = request_path.with_file_name(name);
|
||||
let bytes = std::fs::read(&response_path).expect("response file written");
|
||||
@ -375,6 +390,30 @@ async fn valid_spawn_request_succeeds_and_is_consumed() {
|
||||
assert_eq!(contexts.entries()[0].name, "dev-backend");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn valid_agent_run_request_succeeds_and_reports_type() {
|
||||
let tmp = TempDir::new();
|
||||
let contexts = FakeContexts::new();
|
||||
let service = build_service(contexts.clone());
|
||||
|
||||
let req = tmp.0.join("req-agent-run.json");
|
||||
std::fs::write(
|
||||
&req,
|
||||
br#"{ "type": "agent.run", "requestedBy": "Main", "targetAgent": "architect", "profile": "claude-code", "task": "Analyse", "visibility": "background" }"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let response = process_request_file(&req, &project(), &service).await;
|
||||
|
||||
assert!(response.ok, "expected ok, got {response:?}");
|
||||
assert_eq!(response.action.as_deref(), Some("agent.run"));
|
||||
assert!(!req.exists(), "request file must be removed");
|
||||
let on_disk = read_response(&req);
|
||||
assert!(on_disk.ok);
|
||||
assert_eq!(contexts.entries().len(), 1);
|
||||
assert_eq!(contexts.entries()[0].name, "architect");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn valid_create_skill_request_succeeds_and_is_consumed() {
|
||||
let tmp = TempDir::new();
|
||||
@ -408,7 +447,11 @@ async fn invalid_json_request_yields_error_response() {
|
||||
|
||||
assert!(!response.ok);
|
||||
assert!(
|
||||
response.error.as_deref().unwrap_or_default().contains("invalid json"),
|
||||
response
|
||||
.error
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.contains("invalid json"),
|
||||
"got {response:?}"
|
||||
);
|
||||
assert!(!req.exists(), "poisoned request must still be removed");
|
||||
@ -427,5 +470,9 @@ async fn unknown_action_yields_error_response() {
|
||||
|
||||
assert!(!response.ok);
|
||||
assert_eq!(response.action.as_deref(), Some("explode"));
|
||||
assert!(response.error.as_deref().unwrap_or_default().contains("unknown orchestrator action"));
|
||||
assert!(response
|
||||
.error
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.contains("unknown orchestrator action"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user