feat(wave): #119/#122/#131/#132 verts + sprint plugins ESM/persistance #135/#136/#139
État d'intégration confiné à la branche batch. Les tickets #119 (skills → capacités agent découvrables), #122 (override permissions par défaut), #131 (effort par agent/presets) et #132 (outil MCP d'édition du contexte projet) sont verts en périmètre. Le sprint plugins multi-fichiers ESM / persistance plugin-owned (#135/#136/#139) est co-implémenté dans les MÊMES fichiers de câblage (frontend/src/ports/index.ts, backend/src/lib.rs, domain/ports.rs, backend/dto.rs), inséparable sans staging interactif (indisponible ici). Commit unique volontaire : préserve l'état vert QA sans découpe hunk risquée. NON mergé vers develop tant que #137 (QA e2e plugins) n'est pas vert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::DomainError;
|
||||
use crate::ids::{AgentId, ProfileId, TemplateId};
|
||||
use crate::profile::EffortSelection;
|
||||
use crate::skill::SkillRef;
|
||||
use crate::template::TemplateVersion;
|
||||
|
||||
@ -70,6 +71,10 @@ pub struct Agent {
|
||||
/// activation (ARCHITECTURE §14.2). Empty by default.
|
||||
#[serde(default)]
|
||||
pub skills: Vec<SkillRef>,
|
||||
/// Per-agent effort selection. `None` falls back to the profile's
|
||||
/// `model_reasoning_effort` at launch.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub effort: Option<EffortSelection>,
|
||||
}
|
||||
|
||||
impl Agent {
|
||||
@ -104,6 +109,7 @@ impl Agent {
|
||||
origin,
|
||||
synchronized,
|
||||
skills: Vec::new(),
|
||||
effort: None,
|
||||
})
|
||||
}
|
||||
|
||||
@ -128,6 +134,13 @@ impl Agent {
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns a copy of this agent carrying the given per-agent effort override.
|
||||
#[must_use]
|
||||
pub fn with_effort(mut self, effort: Option<EffortSelection>) -> Self {
|
||||
self.effort = effort;
|
||||
self
|
||||
}
|
||||
|
||||
/// Assigns a skill to this agent. Idempotent: re-assigning the same
|
||||
/// `skill_id` is a no-op (returns `false`); a new assignment returns `true`.
|
||||
pub fn assign_skill(&mut self, skill: SkillRef) -> bool {
|
||||
@ -183,6 +196,9 @@ pub struct ManifestEntry {
|
||||
/// backward-compatible deserialisation of pre-L12 manifests.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub skills: Vec<SkillRef>,
|
||||
/// Per-agent effort selection. Missing in older manifests means no override.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub effort: Option<EffortSelection>,
|
||||
}
|
||||
|
||||
impl ManifestEntry {
|
||||
@ -221,6 +237,7 @@ impl ManifestEntry {
|
||||
synchronized,
|
||||
synced_template_version,
|
||||
skills: Vec::new(),
|
||||
effort: None,
|
||||
})
|
||||
}
|
||||
|
||||
@ -246,6 +263,7 @@ impl ManifestEntry {
|
||||
synchronized: agent.synchronized,
|
||||
synced_template_version,
|
||||
skills: agent.skills.clone(),
|
||||
effort: agent.effort.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -271,7 +289,8 @@ impl ManifestEntry {
|
||||
origin,
|
||||
self.synchronized,
|
||||
)?
|
||||
.with_skills(self.skills.clone()))
|
||||
.with_skills(self.skills.clone())
|
||||
.with_effort(self.effort.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -530,4 +549,41 @@ mod orchestrator_tests {
|
||||
&d,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_with_effort_round_trips_through_manifest_entry() {
|
||||
let agent = Agent::new(
|
||||
agent_id(1),
|
||||
"agent-1",
|
||||
"agents/agent-1.md",
|
||||
ProfileId::from_uuid(uuid::Uuid::from_u128(1001)),
|
||||
AgentOrigin::Scratch,
|
||||
false,
|
||||
)
|
||||
.unwrap()
|
||||
.with_effort(Some(EffortSelection::Preset("medium".to_owned())));
|
||||
|
||||
let entry = ManifestEntry::from_agent(&agent);
|
||||
let back = entry.to_agent().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
back.effort,
|
||||
Some(EffortSelection::Preset("medium".to_owned()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_entry_effort_absent_deserialises_as_none() {
|
||||
let json = r#"{
|
||||
"agentId":"00000000-0000-0000-0000-000000000001",
|
||||
"name":"agent-1",
|
||||
"mdPath":"agents/agent-1.md",
|
||||
"profileId":"00000000-0000-0000-0000-0000000003e9",
|
||||
"synchronized":false
|
||||
}"#;
|
||||
|
||||
let entry: ManifestEntry = serde_json::from_str(json).expect("legacy entry deserialises");
|
||||
assert_eq!(entry.effort, None);
|
||||
assert_eq!(entry.to_agent().unwrap().effort, None);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user