feat(model-server): backend modèles locaux — serveur llama.cpp intégré (#35) et profils OpenCode locaux multiples (#36)
Sprint « Modeles locaux », couche backend (domain/application/infra/app-tauri). #35 — Serveur de modèle local intégré (llama.cpp) : - domain: model_server.rs (agrégat + statut), ports ModelServerProbe / ManagedProcess / ModelServerRuntime / ModelServerRegistry, événements model_server_status_changed et agent_launch_failed. - application: use case EnsureLocalModelServer branché sur LaunchAgent. - infrastructure: adapters HttpOpenAiCompatibleProbe, LlamaCppRuntime, LocalManagedProcess, FsModelServerRegistry. - app-tauri: DTO plat LocalModelServerConfigDto, commandes list/save/delete_model_server avec garde model_server_in_use, wiring. #36 — Profils OpenCode locaux multiples : - domain: VO LocalModelServerId, OpenCodeConfig.local_model_server_id. - application: use case CloneOpenCodeProfileFromSeed. - app-tauri: commande clone_opencode_profile_from_seed, DTO/wiring. Tests verts (exécution réelle) : domain+application OK, app-tauri dto_model_servers 3/3 et dto_profiles 10/10, infra model_server 2/2, application model_server+profile_usecases 19/19, cargo build workspace Finished. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -13,12 +13,13 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::ports::{AgentRuntime, ProfileStore};
|
||||
use domain::profile::AgentProfile;
|
||||
use domain::ids::ProfileId;
|
||||
use domain::ports::{AgentRuntime, IdGenerator, ProfileStore};
|
||||
use domain::profile::{AgentProfile, OpenCodeConfig, StructuredAdapter};
|
||||
|
||||
use crate::error::AppError;
|
||||
|
||||
use super::catalogue::selectable_reference_profiles;
|
||||
use super::catalogue::{reference_profile_id, reference_profiles, selectable_reference_profiles};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DetectProfiles
|
||||
@ -133,6 +134,111 @@ pub struct SaveProfileOutput {
|
||||
pub profile: AgentProfile,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CloneOpenCodeProfileFromSeed
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Input for [`CloneOpenCodeProfileFromSeed::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CloneOpenCodeProfileFromSeedInput {
|
||||
/// Optional display name for the cloned profile. When absent, a copy label is
|
||||
/// derived from the seed name.
|
||||
pub name: Option<String>,
|
||||
/// Optional OpenCode config override. When absent, the seed config is copied.
|
||||
pub opencode: Option<OpenCodeConfig>,
|
||||
}
|
||||
|
||||
/// Output of [`CloneOpenCodeProfileFromSeed::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CloneOpenCodeProfileFromSeedOutput {
|
||||
/// The newly persisted profile.
|
||||
pub profile: AgentProfile,
|
||||
}
|
||||
|
||||
/// Creates a new OpenCode profile instance from the canonical seed/template.
|
||||
///
|
||||
/// The persisted canonical `opencode-llamacpp` profile is preferred when present,
|
||||
/// so local edits are preserved as the clone template. If it is absent, the
|
||||
/// in-memory reference catalogue seed is used. The new profile always receives a
|
||||
/// fresh [`ProfileId`], which is the only identity constraint; multiple profiles
|
||||
/// with `StructuredAdapter::OpenCode` are therefore valid.
|
||||
pub struct CloneOpenCodeProfileFromSeed {
|
||||
store: Arc<dyn ProfileStore>,
|
||||
ids: Arc<dyn IdGenerator>,
|
||||
}
|
||||
|
||||
impl CloneOpenCodeProfileFromSeed {
|
||||
/// Builds the use case from the profile store and id generator ports.
|
||||
#[must_use]
|
||||
pub fn new(store: Arc<dyn ProfileStore>, ids: Arc<dyn IdGenerator>) -> Self {
|
||||
Self { store, ids }
|
||||
}
|
||||
|
||||
/// Clones the canonical OpenCode seed into a new persisted profile.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AppError::Store`] on persistence failure, [`AppError::Invalid`] if the
|
||||
/// requested name is blank, or [`AppError::Internal`] if the seed is malformed.
|
||||
pub async fn execute(
|
||||
&self,
|
||||
input: CloneOpenCodeProfileFromSeedInput,
|
||||
) -> Result<CloneOpenCodeProfileFromSeedOutput, AppError> {
|
||||
let existing = self.store.list().await?;
|
||||
let seed_id = reference_profile_id("opencode-llamacpp");
|
||||
let seed = existing
|
||||
.iter()
|
||||
.find(|profile| profile.id == seed_id)
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
reference_profiles()
|
||||
.into_iter()
|
||||
.find(|profile| profile.id == seed_id)
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
AppError::Internal("canonical OpenCode seed `opencode-llamacpp` is missing".into())
|
||||
})?;
|
||||
|
||||
if seed.structured_adapter != Some(StructuredAdapter::OpenCode) || seed.opencode.is_none() {
|
||||
return Err(AppError::Internal(
|
||||
"canonical OpenCode seed is not an OpenCode profile".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut profile = seed;
|
||||
profile.id = fresh_profile_id(&*self.ids, &existing)?;
|
||||
profile.name = match input.name {
|
||||
Some(name) => {
|
||||
if name.trim().is_empty() {
|
||||
return Err(AppError::Invalid("profile.name must not be empty".into()));
|
||||
}
|
||||
name
|
||||
}
|
||||
None => format!("{} copy", profile.name),
|
||||
};
|
||||
if let Some(config) = input.opencode {
|
||||
profile.opencode = Some(config);
|
||||
}
|
||||
|
||||
self.store.save(&profile).await?;
|
||||
Ok(CloneOpenCodeProfileFromSeedOutput { profile })
|
||||
}
|
||||
}
|
||||
|
||||
fn fresh_profile_id(
|
||||
ids: &dyn IdGenerator,
|
||||
existing: &[AgentProfile],
|
||||
) -> Result<ProfileId, AppError> {
|
||||
for _ in 0..16 {
|
||||
let id = ProfileId::from_uuid(ids.new_uuid());
|
||||
if existing.iter().all(|profile| profile.id != id) {
|
||||
return Ok(id);
|
||||
}
|
||||
}
|
||||
Err(AppError::Internal(
|
||||
"could not allocate a unique profile id".into(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Persists (creates or replaces) a single profile.
|
||||
pub struct SaveProfile {
|
||||
store: Arc<dyn ProfileStore>,
|
||||
|
||||
Reference in New Issue
Block a user