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:
@ -6,8 +6,8 @@ mod helpers;
|
||||
use domain::{
|
||||
Agent, AgentManifest, AgentOrigin, AgentProfile, AgentTemplate, ContextInjection, Direction,
|
||||
LayoutNode, LayoutTree, LeafCell, ManifestEntry, MarkdownDoc, Project, ProjectPath, RemoteRef,
|
||||
SessionStrategy, Skill, SkillId, SkillRef, SkillScope, SplitContainer, SshAuth, TemplateVersion,
|
||||
WeightedChild,
|
||||
SessionStrategy, Skill, SkillId, SkillRef, SkillScope, SplitContainer, SshAuth,
|
||||
TemplateVersion, WeightedChild,
|
||||
};
|
||||
use helpers::{node, session};
|
||||
use uuid::Uuid;
|
||||
@ -72,7 +72,14 @@ fn project_uses_camel_case_and_tagged_remote() {
|
||||
|
||||
#[test]
|
||||
fn remote_ssh_roundtrip_and_tags() {
|
||||
let r = RemoteRef::ssh("host", 2222, "me", SshAuth::Key { path: "/k".into() }, "/srv").unwrap();
|
||||
let r = RemoteRef::ssh(
|
||||
"host",
|
||||
2222,
|
||||
"me",
|
||||
SshAuth::Key { path: "/k".into() },
|
||||
"/srv",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(roundtrip(&r), r);
|
||||
let json = serde_json::to_string(&r).unwrap();
|
||||
assert!(json.contains("\"kind\":\"ssh\""), "json was {json}");
|
||||
@ -118,9 +125,12 @@ fn profile_roundtrip_all_injection_variants() {
|
||||
|
||||
#[test]
|
||||
fn context_injection_strategy_tag_is_camel_case() {
|
||||
let json = serde_json::to_string(&ContextInjection::convention_file("CLAUDE.md").unwrap())
|
||||
.unwrap();
|
||||
assert!(json.contains("\"strategy\":\"conventionFile\""), "json was {json}");
|
||||
let json =
|
||||
serde_json::to_string(&ContextInjection::convention_file("CLAUDE.md").unwrap()).unwrap();
|
||||
assert!(
|
||||
json.contains("\"strategy\":\"conventionFile\""),
|
||||
"json was {json}"
|
||||
);
|
||||
let json = serde_json::to_string(&ContextInjection::stdin()).unwrap();
|
||||
assert!(json.contains("\"strategy\":\"stdin\""), "json was {json}");
|
||||
}
|
||||
@ -203,7 +213,10 @@ fn session_assign_flag_omitted_when_none() {
|
||||
let session = SessionStrategy::new(None, "--continue").unwrap();
|
||||
let json = serde_json::to_string(&session).unwrap();
|
||||
assert!(!json.contains("assignFlag"), "json was {json}");
|
||||
assert!(json.contains("\"resumeFlag\":\"--continue\""), "json was {json}");
|
||||
assert!(
|
||||
json.contains("\"resumeFlag\":\"--continue\""),
|
||||
"json was {json}"
|
||||
);
|
||||
assert_eq!(roundtrip(&session), session);
|
||||
}
|
||||
|
||||
@ -244,13 +257,22 @@ fn agent_roundtrip_from_template() {
|
||||
let json = serde_json::to_string(&a).unwrap();
|
||||
assert!(json.contains("\"contextPath\""), "json was {json}");
|
||||
// AgentOrigin tagged with `type`, camelCased.
|
||||
assert!(json.contains("\"type\":\"fromTemplate\""), "json was {json}");
|
||||
assert!(
|
||||
json.contains("\"type\":\"fromTemplate\""),
|
||||
"json was {json}"
|
||||
);
|
||||
// Inner fields must be camelCased per ARCHITECTURE §9.1:
|
||||
// { "type":"fromTemplate", "templateId":"...", "syncedTemplateVersion":N }.
|
||||
assert!(json.contains("\"templateId\""), "json was {json}");
|
||||
assert!(json.contains("\"syncedTemplateVersion\":4"), "json was {json}");
|
||||
assert!(
|
||||
json.contains("\"syncedTemplateVersion\":4"),
|
||||
"json was {json}"
|
||||
);
|
||||
assert!(!json.contains("\"template_id\""), "json was {json}");
|
||||
assert!(!json.contains("\"synced_template_version\""), "json was {json}");
|
||||
assert!(
|
||||
!json.contains("\"synced_template_version\""),
|
||||
"json was {json}"
|
||||
);
|
||||
assert!(!json.contains("\"synced_version\""), "json was {json}");
|
||||
}
|
||||
|
||||
@ -285,14 +307,18 @@ fn manifest_roundtrip_and_camel_case() {
|
||||
Some(TemplateVersion(5)),
|
||||
)
|
||||
.unwrap();
|
||||
let e2 = ManifestEntry::new(aid(3), "Beta", "agents/b.md", profid(9), None, false, None).unwrap();
|
||||
let e2 =
|
||||
ManifestEntry::new(aid(3), "Beta", "agents/b.md", profid(9), None, false, None).unwrap();
|
||||
let m = AgentManifest::new(1, vec![e1, e2]).unwrap();
|
||||
assert_eq!(roundtrip(&m), m);
|
||||
let json = serde_json::to_string(&m).unwrap();
|
||||
// entries are serialized under "agents".
|
||||
assert!(json.contains("\"agents\":["), "json was {json}");
|
||||
assert!(json.contains("\"mdPath\""), "json was {json}");
|
||||
assert!(json.contains("\"syncedTemplateVersion\":5"), "json was {json}");
|
||||
assert!(
|
||||
json.contains("\"syncedTemplateVersion\":5"),
|
||||
"json was {json}"
|
||||
);
|
||||
// Non-synchronized entry omits optional template fields (skip_serializing_if).
|
||||
assert!(!json.contains("\"templateId\":null"), "json was {json}");
|
||||
}
|
||||
@ -307,13 +333,25 @@ fn sid(n: u128) -> SkillId {
|
||||
|
||||
#[test]
|
||||
fn skill_roundtrip_and_camel_case_scope() {
|
||||
let s = Skill::new(sid(1), "code-review", MarkdownDoc::new("body"), SkillScope::Global).unwrap();
|
||||
let s = Skill::new(
|
||||
sid(1),
|
||||
"code-review",
|
||||
MarkdownDoc::new("body"),
|
||||
SkillScope::Global,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(roundtrip(&s), s);
|
||||
let json = serde_json::to_string(&s).unwrap();
|
||||
assert!(json.contains("\"scope\":\"global\""), "json was {json}");
|
||||
assert!(json.contains("\"contentMd\""), "json was {json}");
|
||||
|
||||
let p = Skill::new(sid(2), "simplify", MarkdownDoc::new("b"), SkillScope::Project).unwrap();
|
||||
let p = Skill::new(
|
||||
sid(2),
|
||||
"simplify",
|
||||
MarkdownDoc::new("b"),
|
||||
SkillScope::Project,
|
||||
)
|
||||
.unwrap();
|
||||
let pj = serde_json::to_string(&p).unwrap();
|
||||
assert!(pj.contains("\"scope\":\"project\""), "json was {pj}");
|
||||
}
|
||||
@ -412,7 +450,10 @@ fn leaf_with_agent_roundtrip_and_omits_null() {
|
||||
}
|
||||
let json = serde_json::to_string(&tree).unwrap();
|
||||
// agent present when set
|
||||
assert!(json.contains("\"agent\""), "agent field should be present when set; json was {json}");
|
||||
assert!(
|
||||
json.contains("\"agent\""),
|
||||
"agent field should be present when set; json was {json}"
|
||||
);
|
||||
// null variant omitted
|
||||
let tree_no_agent = LayoutTree::new(LayoutNode::Leaf(LeafCell {
|
||||
id: node(2),
|
||||
@ -422,5 +463,8 @@ fn leaf_with_agent_roundtrip_and_omits_null() {
|
||||
agent_was_running: false,
|
||||
}));
|
||||
let json2 = serde_json::to_string(&tree_no_agent).unwrap();
|
||||
assert!(!json2.contains("\"agent\""), "agent field should be omitted when None; json was {json2}");
|
||||
assert!(
|
||||
!json2.contains("\"agent\""),
|
||||
"agent field should be omitted when None; json was {json2}"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user