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

@ -32,7 +32,7 @@ use domain::{
};
use crate::error::AppError;
use crate::terminal::{LiveSessionKind, LiveSessionSnapshot, LiveSessions};
use crate::terminal::{LiveRuntimeState, LiveSessionKind, LiveSessionSnapshot, LiveSessions};
use crate::HandoffProvider;
/// Maximum length (in characters) of a derived [`AgentTicketState::task_preview`].
@ -202,6 +202,8 @@ pub struct AgentWorkState {
pub profile_id: ProfileId,
/// Current live session, if the agent is live.
pub live: Option<LiveWorkSession>,
/// Last known runtime lifecycle state for the agent, distinct from `live`.
pub runtime_state: Option<LiveRuntimeState>,
/// Current FIFO/busy state.
pub busy: AgentBusyState,
/// Pending/in-progress delegation tickets, in FIFO order.
@ -490,6 +492,10 @@ impl GetProjectWorkState {
session_id: snapshot.session_id,
kind: snapshot.kind,
});
let runtime_state = self
.live
.runtime_status_for(input.project.id, &agent.id)
.map(|status| status.state);
// Tickets are crossed with the busy state: only an agent absent from
// the manifest is dropped (this loop only visits manifest entries), so
// the manifest boundary is naturally preserved.
@ -510,6 +516,7 @@ impl GetProjectWorkState {
name: agent.name,
profile_id: agent.profile_id,
live,
runtime_state,
busy,
tickets,
background_tasks,