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:
2026-07-11 15:50:08 +02:00
parent 2e98f1fbb9
commit b82ac76f8b
24 changed files with 3147 additions and 94 deletions

View File

@ -6,8 +6,8 @@
//! with one error shape when building its `ErrorDTO`.
use domain::ports::{
AgentSessionError, EmbedderError, FsError, GitError, MemoryError, ProcessError, PtyError,
RemoteError, RuntimeError, StoreError,
AgentSessionError, EmbedderError, FsError, GitError, MemoryError, ModelServerError,
ProcessError, PtyError, RemoteError, RuntimeError, StoreError,
};
use domain::{AgentId, NodeId};
use domain::{IssueStoreError, SprintStoreError};
@ -38,6 +38,15 @@ pub enum AppError {
#[error("process error: {0}")]
Process(String),
/// A local model server could not be prepared for launch.
#[error("model server error ({code}): {message}")]
ModelServer {
/// Stable model-server error code.
code: String,
/// Human-readable message.
message: String,
},
/// A git operation failed.
#[error("git error: {0}")]
Git(String),
@ -101,6 +110,7 @@ impl AppError {
Self::FileSystem(_) => "FILESYSTEM",
Self::Store(_) => "STORE",
Self::Process(_) => "PROCESS",
Self::ModelServer { .. } => "MODEL_SERVER",
Self::Git(_) => "GIT",
Self::Remote(_) => "REMOTE",
Self::AgentAlreadyRunning { .. } => "AGENT_ALREADY_RUNNING",
@ -183,6 +193,30 @@ impl From<ProcessError> for AppError {
}
}
impl From<ModelServerError> for AppError {
fn from(e: ModelServerError) -> Self {
Self::ModelServer {
code: model_server_error_code(&e).to_owned(),
message: e.to_string(),
}
}
}
fn model_server_error_code(err: &ModelServerError) -> &'static str {
match err {
ModelServerError::NotConfigured => "not_configured",
ModelServerError::Invalid(_) => "invalid",
ModelServerError::PermissionDenied(_) => "permission_denied",
ModelServerError::PathNotAccessible(_) => "path_not_accessible",
ModelServerError::PortOccupied(_) => "port_occupied",
ModelServerError::InUse(_) => "model_server_in_use",
ModelServerError::Probe(_) => "probe",
ModelServerError::Process(_) => "process",
ModelServerError::Store(_) => "store",
ModelServerError::Timeout => "timeout",
}
}
impl From<RuntimeError> for AppError {
fn from(e: RuntimeError) -> Self {
Self::Process(e.to_string())