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:
@ -35,6 +35,7 @@ use domain::live_state::WorkStatus;
|
||||
|
||||
use crate::error::AppError;
|
||||
use crate::layout::{persist_doc, resolve_doc};
|
||||
use crate::model_server::{EnsureLocalModelServer, EnsureLocalModelServerInput};
|
||||
use crate::project::project_context_path;
|
||||
use crate::terminal::{StructuredSessions, TerminalSessions};
|
||||
use crate::workstate::GetLiveStateLean;
|
||||
@ -1136,6 +1137,10 @@ pub struct LaunchAgent {
|
||||
/// injection (zéro régression). Best-effort strict : provider absent / erreur /
|
||||
/// parse ⇒ section omise, jamais d'échec de lancement.
|
||||
live_state_lean: Option<Arc<dyn LiveStateLeanProvider>>,
|
||||
/// Ensure local model servers referenced by OpenCode profiles before writing
|
||||
/// `opencode.json` (B35). Optional for legacy tests/wiring; required at runtime
|
||||
/// when `OpenCodeConfig.localModelServerId` is set.
|
||||
local_model_server: Option<Arc<EnsureLocalModelServer>>,
|
||||
}
|
||||
|
||||
impl LaunchAgent {
|
||||
@ -1174,9 +1179,17 @@ impl LaunchAgent {
|
||||
provider_sessions: None,
|
||||
projectors: None,
|
||||
live_state_lean: None,
|
||||
local_model_server: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Injects the local model-server ensure use case used by OpenCode profiles.
|
||||
#[must_use]
|
||||
pub fn with_local_model_server(mut self, ensure: Arc<EnsureLocalModelServer>) -> Self {
|
||||
self.local_model_server = Some(ensure);
|
||||
self
|
||||
}
|
||||
|
||||
/// Branche le provider de **live-state lean (lot LS4)** : au lancement, l'aperçu
|
||||
/// `# État du projet` (status + intent des autres agents) est injecté dans le
|
||||
/// convention file. Sans cet appel (cas legacy / tests existants), aucune section
|
||||
@ -1550,7 +1563,7 @@ impl LaunchAgent {
|
||||
.contexts
|
||||
.read_context(&input.project, &agent.id)
|
||||
.await?;
|
||||
let profile = self
|
||||
let mut profile = self
|
||||
.profiles
|
||||
.list()
|
||||
.await?
|
||||
@ -1649,6 +1662,9 @@ impl LaunchAgent {
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.ensure_local_model_server_for_opencode(&agent, &mut profile)
|
||||
.await?;
|
||||
|
||||
// 5a. ── INJECTION DE LA CONF MCP (cadrage v3, Décision 3) ──
|
||||
// Strictement APRÈS le convention file (étape 5) et AVANT le spawn /
|
||||
// `factory.start` (étapes 5b/6). Si le profil porte une `McpCapability`,
|
||||
@ -2336,6 +2352,60 @@ impl LaunchAgent {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_local_model_server_for_opencode(
|
||||
&self,
|
||||
agent: &Agent,
|
||||
profile: &mut AgentProfile,
|
||||
) -> Result<(), AppError> {
|
||||
if profile.structured_adapter != Some(StructuredAdapter::OpenCode) {
|
||||
return Ok(());
|
||||
}
|
||||
let Some(opencode) = profile.opencode.as_mut() else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(server_id) = opencode.local_model_server_id else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(ensure) = self.local_model_server.as_ref() else {
|
||||
let err = AppError::ModelServer {
|
||||
code: "not_wired".to_owned(),
|
||||
message:
|
||||
"OpenCode profile references localModelServerId but no model server service is wired"
|
||||
.to_owned(),
|
||||
};
|
||||
self.publish_agent_launch_failed(agent.id, &err);
|
||||
return Err(err);
|
||||
};
|
||||
|
||||
match ensure
|
||||
.execute(EnsureLocalModelServerInput { server_id })
|
||||
.await
|
||||
{
|
||||
Ok(output) => {
|
||||
opencode.base_url = output.ready.base_url;
|
||||
opencode.model = output.ready.model;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
self.publish_agent_launch_failed(agent.id, &err);
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn publish_agent_launch_failed(&self, agent_id: AgentId, err: &AppError) {
|
||||
let (cause, code) = match err {
|
||||
AppError::ModelServer { code, .. } => ("model_server", code.as_str()),
|
||||
_ => ("launch", err.code()),
|
||||
};
|
||||
self.events.publish(DomainEvent::AgentLaunchFailed {
|
||||
agent_id,
|
||||
cause: cause.to_owned(),
|
||||
code: code.to_owned(),
|
||||
message: err.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Outcome of the R0a discrimination between a legitimate **view reattach** and a
|
||||
|
||||
Reference in New Issue
Block a user