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:
@ -963,6 +963,7 @@ impl ChangeAgentProfile {
|
||||
mcp_runtime: None,
|
||||
allow_structured_alongside_pty: false,
|
||||
require_structured: false,
|
||||
fresh_start: false,
|
||||
})
|
||||
.await?;
|
||||
Ok(Some(output.session))
|
||||
@ -1143,6 +1144,9 @@ pub struct LaunchAgentInput {
|
||||
///
|
||||
/// Historical callers leave this `false`, preserving the PTY launch contract.
|
||||
pub require_structured: bool,
|
||||
/// Cold restart request: ignore the cell's previous conversation id and create
|
||||
/// a new provider/CLI conversation for this activation.
|
||||
pub fresh_start: bool,
|
||||
}
|
||||
|
||||
/// OS/runtime facts injected by the composition root (`app-tauri`) to materialise
|
||||
@ -1863,6 +1867,11 @@ impl LaunchAgent {
|
||||
self.structured_routing_mode,
|
||||
StructuredRoutingMode::RequireStructured
|
||||
);
|
||||
let launch_conversation_id = if input.fresh_start {
|
||||
None
|
||||
} else {
|
||||
input.conversation_id.clone()
|
||||
};
|
||||
|
||||
// 1b. Enforce the "one live session per agent" invariant (decision: an
|
||||
// agent is a singleton that runs in a single cell at a time). This
|
||||
@ -1908,7 +1917,7 @@ impl LaunchAgent {
|
||||
self.pty.kill(&handle).await?;
|
||||
}
|
||||
} else {
|
||||
match reattach_decision(input.node_id, host_node, input.conversation_id.as_deref())
|
||||
match reattach_decision(input.node_id, host_node, launch_conversation_id.as_deref())
|
||||
{
|
||||
ReattachDecision::Rebind { node_id } => {
|
||||
if let Some(session) = self.sessions.rebind_agent_node_in_project(
|
||||
@ -1971,7 +1980,7 @@ impl LaunchAgent {
|
||||
reattach_decision(
|
||||
input.node_id,
|
||||
host_node,
|
||||
input.conversation_id.as_deref(),
|
||||
launch_conversation_id.as_deref(),
|
||||
)
|
||||
};
|
||||
let node_id = match decision {
|
||||
@ -2061,7 +2070,11 @@ impl LaunchAgent {
|
||||
// the cell's current `conversation_id`. Any id this launch *assigns* is
|
||||
// returned in the output so the caller persists it on the leaf.
|
||||
let (session_plan, assigned_conversation_id) = self
|
||||
.resolve_session_plan(&profile, input.conversation_id.clone(), &input.project.root)
|
||||
.resolve_session_plan(
|
||||
&profile,
|
||||
launch_conversation_id.clone(),
|
||||
&input.project.root,
|
||||
)
|
||||
.await;
|
||||
let mut spec =
|
||||
self.runtime
|
||||
@ -2076,7 +2089,7 @@ impl LaunchAgent {
|
||||
&manifest,
|
||||
&agent,
|
||||
content.clone(),
|
||||
input.conversation_id.as_deref(),
|
||||
launch_conversation_id.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
// Best-effort contextual embedder suggestion (LOT C3, §14.5.5): the agent
|
||||
@ -2186,7 +2199,7 @@ impl LaunchAgent {
|
||||
// pure partagée avec `resolve_conversation` (aucun couplage à
|
||||
// l'orchestrateur). C'est cet id — et **non** l'id de session moteur —
|
||||
// qui retrouve log + handoff au (re)lancement (P7) et survit au swap.
|
||||
let pair_conversation_id = input.conversation_id.clone().unwrap_or_else(|| {
|
||||
let pair_conversation_id = launch_conversation_id.clone().unwrap_or_else(|| {
|
||||
ConversationId::for_project_pair(
|
||||
input.project.id,
|
||||
ConversationParty::User,
|
||||
|
||||
Reference in New Issue
Block a user