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

@ -34,12 +34,15 @@ use crate::background_task::{
BackgroundTask, BackgroundTaskKind, BackgroundTaskResult, BackgroundTaskWakePolicy,
};
use crate::events::DomainEvent;
use crate::ids::{AgentId, NodeId, ProjectId, ScheduleId, SessionId, SprintId, TaskId};
use crate::ids::{
AgentId, LocalModelServerId, NodeId, ProjectId, ScheduleId, SessionId, SprintId, TaskId,
};
use crate::issue::{
Issue, IssueCarnet, IssueIndexEntry, IssueListFilter, IssueNumber, IssueRef, IssueVersion,
};
use crate::markdown::MarkdownDoc;
use crate::memory::{Memory, MemoryIndexEntry, MemoryLink, MemorySlug};
use crate::model_server::{LocalModelServerConfig, ModelServerEndpoint, ModelServerStatus};
use crate::permission::ProjectPermissions;
use crate::profile::{AgentProfile, EmbedderProfile};
use crate::project::{Project, ProjectPath};
@ -206,6 +209,27 @@ pub struct Output {
pub stderr: Vec<u8>,
}
/// Opaque handle for a managed local process.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ManagedProcessHandle {
/// Stable local process identity as assigned by the adapter.
pub id: String,
}
/// Observed status of a managed local process.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProcessStatus {
/// Process is still running.
Running,
/// Process has exited.
Exited {
/// Exit code, when available.
code: Option<i32>,
},
/// Handle is unknown to the adapter.
Unknown,
}
/// A location-neutral path used by [`FileSystem`] (local, SFTP, or WSL).
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RemotePath(pub String);
@ -502,6 +526,41 @@ pub enum ProcessError {
Io(String),
}
/// Errors from local model-server ports.
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum ModelServerError {
/// The referenced configuration does not exist.
#[error("not configured")]
NotConfigured,
/// Server or configuration is invalid.
#[error("invalid: {0}")]
Invalid(String),
/// Filesystem permission denied.
#[error("permission denied: {0}")]
PermissionDenied(String),
/// Required path is not accessible.
#[error("path not accessible: {0}")]
PathNotAccessible(String),
/// The configured port is already in use by another managed server.
#[error("port occupied: {0}")]
PortOccupied(u16),
/// The server is still referenced by another configuration object.
#[error("model server in use: {0}")]
InUse(String),
/// Readiness probe failed unexpectedly.
#[error("probe failed: {0}")]
Probe(String),
/// Process operation failed.
#[error("process failed: {0}")]
Process(String),
/// Store operation failed.
#[error("store failed: {0}")]
Store(String),
/// Readiness timed out.
#[error("readiness timed out")]
Timeout,
}
/// Errors from [`FileSystem`].
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum FsError {
@ -914,6 +973,88 @@ pub trait ProcessSpawner: Send + Sync {
async fn run(&self, spec: SpawnSpec) -> Result<Output, ProcessError>;
}
/// Probe readiness of an OpenAI-compatible model server.
#[async_trait]
pub trait ModelServerProbe: Send + Sync {
/// Probes `endpoint` and returns the observed status.
///
/// # Errors
/// [`ModelServerError`] on unexpected probe failure. An unreachable server
/// should normally return [`ModelServerStatus::Unreachable`].
async fn probe(
&self,
endpoint: &ModelServerEndpoint,
) -> Result<ModelServerStatus, ModelServerError>;
}
/// Manages local long-lived child processes.
#[async_trait]
pub trait ManagedProcess: Send + Sync {
/// Spawns a process from an argv-structured spec.
///
/// # Errors
/// [`ModelServerError`] on spawn failure.
async fn spawn(&self, spec: SpawnSpec) -> Result<ManagedProcessHandle, ModelServerError>;
/// Kills a process.
///
/// # Errors
/// [`ModelServerError`] on kill failure.
async fn kill(&self, handle: &ManagedProcessHandle) -> Result<(), ModelServerError>;
/// Returns the status of a process.
///
/// # Errors
/// [`ModelServerError`] on status failure.
async fn status(
&self,
handle: &ManagedProcessHandle,
) -> Result<ProcessStatus, ModelServerError>;
}
/// Builds the argv-structured spawn spec for a local model server.
pub trait ModelServerRuntime: Send + Sync {
/// Builds a spawn spec from a validated local-server config.
///
/// # Errors
/// [`ModelServerError`] if the config cannot be launched.
fn build_spawn_spec(
&self,
config: &LocalModelServerConfig,
) -> Result<SpawnSpec, ModelServerError>;
}
/// Persists local model-server configuration in the global IdeA store.
#[async_trait]
pub trait ModelServerRegistry: Send + Sync {
/// Returns one server config by id.
///
/// # Errors
/// [`ModelServerError`] on store failure.
async fn get(
&self,
id: &LocalModelServerId,
) -> Result<Option<LocalModelServerConfig>, ModelServerError>;
/// Lists all server configs.
///
/// # Errors
/// [`ModelServerError`] on store failure.
async fn list(&self) -> Result<Vec<LocalModelServerConfig>, ModelServerError>;
/// Saves one server config.
///
/// # Errors
/// [`ModelServerError`] on store failure.
async fn save(&self, config: LocalModelServerConfig) -> Result<(), ModelServerError>;
/// Deletes one server config.
///
/// # Errors
/// [`ModelServerError`] on store failure.
async fn delete(&self, id: LocalModelServerId) -> Result<(), ModelServerError>;
}
/// Location-neutral filesystem access.
#[async_trait]
pub trait FileSystem: Send + Sync {