fix(runtime): stop/restart d'agent visible robuste — kill réel, état stopped, relance froide (#196, QA verte)

Solidifie le cycle stop/restart d'un agent visible en cellule:

- terminal/registry: kill effectif de la TUI native et de la CLI custom, arrêt idempotent du live runtime (PTY + structuré), distinction nette « agent stoppé » vs « pas de session ».

- workstate + lifecycle: projection d'un état runtime explicite « stopped » (AgentRuntimeState: running/stopping/stopped/failedStart), réconcilié avec le live state; traitement des courses « agent not found ».

- relance froide via freshStart: repart de zéro sans réattacher une session morte et recharge la configuration courante de l'agent (y compris changement de modèle).

- contrat projeté côté adapters frontend (ports/domain/normalization/mock) pour la consommation UI future (#197/#198), sans surface UI ici.

Tests QA verts: workstate_actions, orchestrator_service stop_agent, agent_lifecycle (reopen/fresh_start), workstate stopped runtime state, vitest adapter stop_live_agent.
This commit is contained in:
2026-08-12 14:56:02 +02:00
parent 4e93fa2f20
commit bad7b4c2f3
23 changed files with 440 additions and 46 deletions

View File

@ -15,8 +15,9 @@ use application::{
ConversationPreviewStatus, ConversationTurnWorkPreview, ConversationWorkSummary,
CreateProjectInput, CreateProjectOutput, ExecuteSlashCommandOutput, GitGraphOutput,
HealthInput, HealthReport, LayoutKind, ListProjectsOutput, ListSlashCommandsOutput,
LiveSessionKind, LiveSessionSnapshot, OpenProjectOutput, ProjectWorkState, SlashCommandEffect,
StopLiveAgentOutput, TicketWorkSource, TicketWorkStatus, TurnPage, TurnSource, TurnView,
LiveRuntimeState, LiveSessionKind, LiveSessionSnapshot, OpenProjectOutput, ProjectWorkState,
SlashCommandEffect, StopLiveAgentOutput, TicketWorkSource, TicketWorkStatus, TurnPage,
TurnSource, TurnView,
};
use domain::ports::{ReplyProgress, ReplyProgressKind, ReplyProgressSource, ReplyProgressStage};
use domain::{
@ -2962,6 +2963,10 @@ pub struct LaunchAgentRequestDto {
/// view sends `"chat"` to require a structured/headless session.
#[serde(default)]
pub cell_kind: Option<CellKind>,
/// Cold restart request: ignore any cell conversation id and create a fresh
/// provider/CLI conversation after the caller has stopped the previous runtime.
#[serde(default)]
pub fresh_start: bool,
}
impl From<LaunchAgentOutput> for TerminalSessionDto {
@ -3452,6 +3457,8 @@ pub struct LiveAgentDto {
pub session_id: String,
/// Runtime family that owns the session (`pty`/`structured`).
pub kind: LiveWorkSessionKindDto,
/// Runtime lifecycle state. Live agents are always `running`.
pub runtime_state: LiveRuntimeStateDto,
}
/// Response DTO for `list_live_agents` (transparent array on the wire).
@ -3479,6 +3486,7 @@ impl LiveAgentListDto {
node_id: snapshot.node_id.to_string(),
session_id: snapshot.session_id.to_string(),
kind: snapshot.kind.into(),
runtime_state: LiveRuntimeStateDto::Running,
})
.collect(),
)
@ -3504,6 +3512,31 @@ impl From<LiveSessionKind> for LiveWorkSessionKindDto {
}
}
/// Explicit lifecycle state for a visible agent runtime.
#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum LiveRuntimeStateDto {
/// A live session exists in one runtime registry.
Running,
/// A stop request is in progress.
Stopping,
/// The last live session was torn down.
Stopped,
/// The last launch attempt failed before registration.
FailedStart,
}
impl From<LiveRuntimeState> for LiveRuntimeStateDto {
fn from(state: LiveRuntimeState) -> Self {
match state {
LiveRuntimeState::Running => Self::Running,
LiveRuntimeState::Stopping => Self::Stopping,
LiveRuntimeState::Stopped => Self::Stopped,
LiveRuntimeState::FailedStart => Self::FailedStart,
}
}
}
/// Live session coordinates in the project work-state read model.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
@ -3514,6 +3547,8 @@ pub struct LiveWorkSessionDto {
pub session_id: String,
/// Runtime family that owns the session.
pub kind: LiveWorkSessionKindDto,
/// Runtime lifecycle state. Live sessions are always `running`.
pub runtime_state: LiveRuntimeStateDto,
}
/// Derived processing status of a queued ticket.
@ -3665,6 +3700,8 @@ pub struct AgentWorkStateDto {
pub profile_id: String,
/// Live session, if any.
pub live: Option<LiveWorkSessionDto>,
/// Last known runtime lifecycle state, distinct from live-session presence.
pub runtime_state: Option<LiveRuntimeStateDto>,
/// Current mediated-input busy state.
pub busy: AgentBusyState,
/// Pending/in-progress delegation tickets, in FIFO order.
@ -3789,7 +3826,9 @@ impl From<ProjectWorkState> for ProjectWorkStateDto {
node_id: live.node_id.to_string(),
session_id: live.session_id.to_string(),
kind: live.kind.into(),
runtime_state: LiveRuntimeStateDto::Running,
}),
runtime_state: agent.runtime_state.map(Into::into),
busy: agent.busy,
tickets: agent
.tickets
@ -3976,6 +4015,8 @@ pub struct StopLiveAgentResponseDto {
pub session_id: String,
/// Runtime family that owned the session (`pty`/`structured`).
pub kind: LiveWorkSessionKindDto,
/// Runtime lifecycle projection after the stop request.
pub runtime_state: LiveRuntimeStateDto,
}
impl From<StopLiveAgentOutput> for StopLiveAgentResponseDto {
@ -3984,6 +4025,7 @@ impl From<StopLiveAgentOutput> for StopLiveAgentResponseDto {
agent_id: out.agent_id.to_string(),
session_id: out.session_id.to_string(),
kind: out.kind.into(),
runtime_state: out.runtime_state.into(),
}
}
}

View File

@ -730,6 +730,7 @@ impl WakeSessionProvider for AppWakeSessionProvider {
mcp_runtime: None,
allow_structured_alongside_pty: true,
require_structured: false,
fresh_start: false,
})
.await
.map_err(|err| WakeError::Session(err.to_string()))?;
@ -850,6 +851,7 @@ impl AgentResumer for AppAgentResumer {
mcp_runtime,
allow_structured_alongside_pty: false,
require_structured: false,
fresh_start: false,
})
.await?;