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:
@ -17,7 +17,8 @@ use std::sync::{Arc, Mutex};
|
||||
use async_trait::async_trait;
|
||||
use domain::agent::{Agent, AgentManifest, AgentOrigin, ManifestEntry};
|
||||
use domain::events::DomainEvent;
|
||||
use domain::ids::{AgentId, ProfileId, ProjectId};
|
||||
use domain::ids::SkillId;
|
||||
use domain::ids::{AgentId, NodeId, ProfileId, ProjectId};
|
||||
use domain::markdown::MarkdownDoc;
|
||||
use domain::ports::{
|
||||
AgentContextStore, AgentRuntime, ContextInjectionPlan, DirEntry, EventBus, EventStream,
|
||||
@ -25,7 +26,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;
|
||||
@ -65,7 +65,10 @@ impl FakeContexts {
|
||||
let me = Self::new();
|
||||
{
|
||||
let mut inner = me.0.lock().unwrap();
|
||||
inner.manifest.entries.push(ManifestEntry::from_agent(agent));
|
||||
inner
|
||||
.manifest
|
||||
.entries
|
||||
.push(ManifestEntry::from_agent(agent));
|
||||
inner
|
||||
.contents
|
||||
.insert(agent.context_path.clone(), content.to_owned());
|
||||
@ -160,7 +163,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(
|
||||
@ -206,7 +213,11 @@ struct RecordingSkills(Arc<Mutex<Vec<Skill>>>);
|
||||
|
||||
#[async_trait]
|
||||
impl SkillStore for RecordingSkills {
|
||||
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(self.0.lock().unwrap().clone())
|
||||
}
|
||||
async fn get(
|
||||
@ -287,14 +298,19 @@ impl FileSystem for FakeFs {
|
||||
struct FakePty {
|
||||
next_id: SessionId,
|
||||
kills: Arc<Mutex<Vec<SessionId>>>,
|
||||
spawns: Arc<Mutex<Vec<SessionId>>>,
|
||||
}
|
||||
impl FakePty {
|
||||
fn new(next_id: SessionId) -> Self {
|
||||
Self {
|
||||
next_id,
|
||||
kills: Arc::new(Mutex::new(Vec::new())),
|
||||
spawns: Arc::new(Mutex::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
fn spawns(&self) -> Vec<SessionId> {
|
||||
self.spawns.lock().unwrap().clone()
|
||||
}
|
||||
fn kills(&self) -> Vec<SessionId> {
|
||||
self.kills.lock().unwrap().clone()
|
||||
}
|
||||
@ -302,6 +318,7 @@ impl FakePty {
|
||||
#[async_trait]
|
||||
impl PtyPort for FakePty {
|
||||
async fn spawn(&self, _spec: SpawnSpec, _size: PtySize) -> Result<PtyHandle, PtyError> {
|
||||
self.spawns.lock().unwrap().push(self.next_id);
|
||||
Ok(PtyHandle {
|
||||
session_id: self.next_id,
|
||||
})
|
||||
@ -368,6 +385,9 @@ fn aid(n: u128) -> AgentId {
|
||||
fn sid(n: u128) -> SessionId {
|
||||
SessionId::from_uuid(Uuid::from_u128(n))
|
||||
}
|
||||
fn nid(n: u128) -> NodeId {
|
||||
NodeId::from_uuid(Uuid::from_u128(n))
|
||||
}
|
||||
|
||||
fn project() -> Project {
|
||||
Project::new(
|
||||
@ -430,6 +450,7 @@ fn fixture(contexts: FakeContexts) -> Fixture {
|
||||
Arc::new(bus.clone()),
|
||||
Arc::new(SeqIds::new()),
|
||||
Arc::new(FakeRecall),
|
||||
None,
|
||||
));
|
||||
let list = Arc::new(ListAgents::new(Arc::new(contexts.clone())));
|
||||
let close = Arc::new(CloseTerminal::new(
|
||||
@ -496,11 +517,9 @@ async fn spawn_unknown_agent_creates_then_launches() {
|
||||
assert_eq!(manifest.entries[0].name, "dev-backend");
|
||||
|
||||
assert!(fx.sessions.session(&sid(777)).is_some());
|
||||
let launched = fx
|
||||
.bus
|
||||
.events()
|
||||
.into_iter()
|
||||
.any(|e| matches!(e, DomainEvent::AgentLaunched { session_id, .. } if session_id == sid(777)));
|
||||
let launched = fx.bus.events().into_iter().any(
|
||||
|e| matches!(e, DomainEvent::AgentLaunched { session_id, .. } if session_id == sid(777)),
|
||||
);
|
||||
assert!(launched, "AgentLaunched must be published");
|
||||
}
|
||||
|
||||
@ -523,6 +542,57 @@ async fn spawn_known_agent_launches_without_recreating() {
|
||||
assert!(fx.sessions.session(&sid(777)).is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn agent_run_existing_agent_does_not_require_profile() {
|
||||
let agent = scratch_agent(aid(1), "architect", "agents/architect.md");
|
||||
let fx = fixture(FakeContexts::with_agent(&agent, "# persona"));
|
||||
|
||||
fx.service
|
||||
.dispatch(
|
||||
&project(),
|
||||
cmd(r#"{ "type":"agent.run", "targetAgent":"architect", "task":"Analyse" }"#),
|
||||
)
|
||||
.await
|
||||
.expect("dispatch ok");
|
||||
|
||||
assert_eq!(fx.contexts.manifest().entries.len(), 1);
|
||||
assert!(fx.sessions.session(&sid(777)).is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn agent_run_visible_reattaches_existing_session() {
|
||||
let agent = scratch_agent(aid(1), "architect", "agents/architect.md");
|
||||
let fx = fixture(FakeContexts::with_agent(&agent, "# persona"));
|
||||
let first_cell = nid(10);
|
||||
let next_cell = nid(20);
|
||||
|
||||
fx.service
|
||||
.dispatch(
|
||||
&project(),
|
||||
cmd(&format!(
|
||||
r#"{{ "action":"spawn_agent", "name":"architect", "profile":"claude", "visibility":"visible", "nodeId":"{first_cell}" }}"#
|
||||
)),
|
||||
)
|
||||
.await
|
||||
.expect("first launch ok");
|
||||
|
||||
let out = fx
|
||||
.service
|
||||
.dispatch(
|
||||
&project(),
|
||||
cmd(&format!(
|
||||
r#"{{ "type":"agent.run", "targetAgent":"architect", "visibility":"visible", "attachToCell":"{next_cell}" }}"#
|
||||
)),
|
||||
)
|
||||
.await
|
||||
.expect("reattach ok");
|
||||
|
||||
assert!(out.detail.contains("attached agent architect"));
|
||||
let session = fx.sessions.session(&sid(777)).expect("live session");
|
||||
assert_eq!(session.node_id, next_cell);
|
||||
assert_eq!(fx.pty.spawns().len(), 1, "reattach must not respawn");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stop_agent_kills_the_right_session() {
|
||||
let agent = scratch_agent(aid(1), "dev-backend", "agents/dev-backend.md");
|
||||
|
||||
Reference in New Issue
Block a user