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:
@ -13,13 +13,13 @@ use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use domain::ids::ProfileId;
|
||||
use domain::ports::{
|
||||
AgentRuntime, ContextInjectionPlan, ExitStatus, Output, PreparedContext, ProcessError,
|
||||
ProcessSpawner, RuntimeError, SessionPlan, SpawnSpec,
|
||||
};
|
||||
use domain::profile::{AgentProfile, ContextInjection, SessionStrategy};
|
||||
use domain::project::ProjectPath;
|
||||
use domain::ids::ProfileId;
|
||||
use domain::MarkdownDoc;
|
||||
use infrastructure::CliAgentRuntime;
|
||||
|
||||
@ -98,7 +98,9 @@ fn prepare_convention_file_keeps_args_and_plans_file() {
|
||||
);
|
||||
let root = ProjectPath::new("/home/me/proj").unwrap();
|
||||
|
||||
let spec = rt.prepare_invocation(&p, &ctx(), &root, &SessionPlan::None).unwrap();
|
||||
let spec = rt
|
||||
.prepare_invocation(&p, &ctx(), &root, &SessionPlan::None)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(spec.command, "mycli");
|
||||
assert_eq!(spec.args, vec!["--static", "arg"], "args unchanged");
|
||||
@ -124,7 +126,9 @@ fn prepare_flag_with_path_substitutes_and_splits() {
|
||||
);
|
||||
let root = ProjectPath::new("/p").unwrap();
|
||||
|
||||
let spec = rt.prepare_invocation(&p, &ctx(), &root, &SessionPlan::None).unwrap();
|
||||
let spec = rt
|
||||
.prepare_invocation(&p, &ctx(), &root, &SessionPlan::None)
|
||||
.unwrap();
|
||||
|
||||
// static args first, then the substituted+split flag args.
|
||||
assert_eq!(
|
||||
@ -149,7 +153,9 @@ fn prepare_flag_without_path_is_switch_then_path() {
|
||||
let p = profile(ContextInjection::flag("-f").unwrap(), "{projectRoot}");
|
||||
let root = ProjectPath::new("/p").unwrap();
|
||||
|
||||
let spec = rt.prepare_invocation(&p, &ctx(), &root, &SessionPlan::None).unwrap();
|
||||
let spec = rt
|
||||
.prepare_invocation(&p, &ctx(), &root, &SessionPlan::None)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(spec.args, vec!["--static", "arg", "-f", ".ideai/agent.md"]);
|
||||
assert_eq!(
|
||||
@ -170,9 +176,15 @@ fn prepare_stdin_keeps_args_and_plans_stdin() {
|
||||
let p = profile(ContextInjection::stdin(), "{projectRoot}");
|
||||
let root = ProjectPath::new("/p").unwrap();
|
||||
|
||||
let spec = rt.prepare_invocation(&p, &ctx(), &root, &SessionPlan::None).unwrap();
|
||||
let spec = rt
|
||||
.prepare_invocation(&p, &ctx(), &root, &SessionPlan::None)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(spec.args, vec!["--static", "arg"], "args unchanged for stdin");
|
||||
assert_eq!(
|
||||
spec.args,
|
||||
vec!["--static", "arg"],
|
||||
"args unchanged for stdin"
|
||||
);
|
||||
assert_eq!(spec.context_plan, Some(ContextInjectionPlan::Stdin));
|
||||
}
|
||||
|
||||
@ -189,7 +201,9 @@ fn prepare_env_keeps_args_and_plans_env() {
|
||||
);
|
||||
let root = ProjectPath::new("/p").unwrap();
|
||||
|
||||
let spec = rt.prepare_invocation(&p, &ctx(), &root, &SessionPlan::None).unwrap();
|
||||
let spec = rt
|
||||
.prepare_invocation(&p, &ctx(), &root, &SessionPlan::None)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(spec.args, vec!["--static", "arg"], "args unchanged for env");
|
||||
assert_eq!(
|
||||
@ -207,13 +221,12 @@ fn prepare_env_keeps_args_and_plans_env() {
|
||||
#[test]
|
||||
fn prepare_substitutes_project_root_in_cwd_template() {
|
||||
let rt = pure_runtime();
|
||||
let p = profile(
|
||||
ContextInjection::stdin(),
|
||||
"{projectRoot}/subdir",
|
||||
);
|
||||
let p = profile(ContextInjection::stdin(), "{projectRoot}/subdir");
|
||||
let root = ProjectPath::new("/home/me/proj").unwrap();
|
||||
|
||||
let spec = rt.prepare_invocation(&p, &ctx(), &root, &SessionPlan::None).unwrap();
|
||||
let spec = rt
|
||||
.prepare_invocation(&p, &ctx(), &root, &SessionPlan::None)
|
||||
.unwrap();
|
||||
assert_eq!(spec.cwd.as_str(), "/home/me/proj/subdir");
|
||||
}
|
||||
|
||||
@ -223,7 +236,9 @@ fn prepare_empty_cwd_template_defaults_to_base() {
|
||||
let p = profile(ContextInjection::stdin(), "");
|
||||
let base = ProjectPath::new("/home/me/proj").unwrap();
|
||||
|
||||
let spec = rt.prepare_invocation(&p, &ctx(), &base, &SessionPlan::None).unwrap();
|
||||
let spec = rt
|
||||
.prepare_invocation(&p, &ctx(), &base, &SessionPlan::None)
|
||||
.unwrap();
|
||||
assert_eq!(spec.cwd.as_str(), "/home/me/proj");
|
||||
}
|
||||
|
||||
@ -232,10 +247,15 @@ fn prepare_substitutes_agent_run_dir_in_cwd_template() {
|
||||
// The canonical template (ARCHITECTURE §14.1): `{agentRunDir}` resolves to the
|
||||
// base cwd the launcher passes — the agent's isolated run directory.
|
||||
let rt = pure_runtime();
|
||||
let p = profile(ContextInjection::convention_file("CLAUDE.md").unwrap(), "{agentRunDir}");
|
||||
let p = profile(
|
||||
ContextInjection::convention_file("CLAUDE.md").unwrap(),
|
||||
"{agentRunDir}",
|
||||
);
|
||||
let run_dir = ProjectPath::new("/home/me/proj/.ideai/run/agent-1").unwrap();
|
||||
|
||||
let spec = rt.prepare_invocation(&p, &ctx(), &run_dir, &SessionPlan::None).unwrap();
|
||||
let spec = rt
|
||||
.prepare_invocation(&p, &ctx(), &run_dir, &SessionPlan::None)
|
||||
.unwrap();
|
||||
assert_eq!(spec.cwd.as_str(), "/home/me/proj/.ideai/run/agent-1");
|
||||
}
|
||||
|
||||
@ -362,8 +382,12 @@ fn session_none_profile_adds_nothing_for_any_plan() {
|
||||
|
||||
for plan in [
|
||||
SessionPlan::None,
|
||||
SessionPlan::Assign { conversation_id: "id-1".to_owned() },
|
||||
SessionPlan::Resume { conversation_id: "id-1".to_owned() },
|
||||
SessionPlan::Assign {
|
||||
conversation_id: "id-1".to_owned(),
|
||||
},
|
||||
SessionPlan::Resume {
|
||||
conversation_id: "id-1".to_owned(),
|
||||
},
|
||||
] {
|
||||
let spec = rt.prepare_invocation(&p, &ctx(), &root(), &plan).unwrap();
|
||||
assert_eq!(spec.args, vec!["--static", "arg"], "plan = {plan:?}");
|
||||
@ -383,7 +407,9 @@ fn session_assign_with_flag_emits_flag_and_id() {
|
||||
&p,
|
||||
&ctx(),
|
||||
&root(),
|
||||
&SessionPlan::Assign { conversation_id: "abc".to_owned() },
|
||||
&SessionPlan::Assign {
|
||||
conversation_id: "abc".to_owned(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@ -403,7 +429,9 @@ fn session_resume_with_flag_emits_resume_and_id() {
|
||||
&p,
|
||||
&ctx(),
|
||||
&root(),
|
||||
&SessionPlan::Resume { conversation_id: "abc".to_owned() },
|
||||
&SessionPlan::Resume {
|
||||
conversation_id: "abc".to_owned(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@ -421,7 +449,9 @@ fn session_resume_without_assign_flag_emits_resume_only() {
|
||||
&p,
|
||||
&ctx(),
|
||||
&root(),
|
||||
&SessionPlan::Resume { conversation_id: "abc".to_owned() },
|
||||
&SessionPlan::Resume {
|
||||
conversation_id: "abc".to_owned(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@ -454,7 +484,9 @@ fn session_assign_without_flag_adds_nothing() {
|
||||
&p,
|
||||
&ctx(),
|
||||
&root(),
|
||||
&SessionPlan::Assign { conversation_id: "abc".to_owned() },
|
||||
&SessionPlan::Assign {
|
||||
conversation_id: "abc".to_owned(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@ -471,8 +503,12 @@ fn no_session_profile_is_unaffected_by_any_plan() {
|
||||
|
||||
for plan in [
|
||||
SessionPlan::None,
|
||||
SessionPlan::Assign { conversation_id: "x".to_owned() },
|
||||
SessionPlan::Resume { conversation_id: "x".to_owned() },
|
||||
SessionPlan::Assign {
|
||||
conversation_id: "x".to_owned(),
|
||||
},
|
||||
SessionPlan::Resume {
|
||||
conversation_id: "x".to_owned(),
|
||||
},
|
||||
] {
|
||||
let spec = rt.prepare_invocation(&p, &ctx(), &root(), &plan).unwrap();
|
||||
assert_eq!(spec.args, vec!["--static", "arg"], "plan = {plan:?}");
|
||||
@ -500,7 +536,9 @@ fn session_args_come_after_context_injection_args() {
|
||||
&p,
|
||||
&ctx(),
|
||||
&root(),
|
||||
&SessionPlan::Assign { conversation_id: "abc".to_owned() },
|
||||
&SessionPlan::Assign {
|
||||
conversation_id: "abc".to_owned(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user