fix(orchestrator): la délégation simple vers UX renvoie son Final structuré (#207, QA verte)

Une demande idea_ask_agent vers UX échouait avec « finished without a structured Final response » alors qu'Architect répondait. Deux causes corrigées:

- session/process: un tour CLI terminé en code non nul SANS stdout (ex. erreur 401/auth) renvoyait une sortie vide au lieu du stderr réel; l'orchestrateur concluait alors à un « no Final ». On remonte désormais le stderr effectif (drain sync + async + helper process_exit_message + test dédié).

- orchestrator/service: la branche « ask waiting on background » masquait le premier Final structuré; la réponse synchrone redevient le premier Final (suppression de active_rendezvous_has_background_tasks / mark_rendezvous_task_waiting).

Nettoyage runtime corollaire (#204): arrêt du live runtime stale (PTY + structuré) d'un agent recréé afin que la délégation route vers l'agent courant (LiveSessions, stop_live_agent_runtime_in_project, wiring backend via DeleteAgent::with_runtime_cleanup, lifecycle, workstate cascade).
This commit is contained in:
2026-08-12 14:09:06 +02:00
parent ba176e3cf2
commit 646b9d9284
11 changed files with 673 additions and 203 deletions

View File

@ -15,7 +15,9 @@ use domain::{AgentId, NodeId, Project, RuntimeAgentKey, SessionId};
use crate::error::AppError;
use crate::orchestrator::OrchestratorService;
use crate::terminal::{CloseTerminal, CloseTerminalInput, LiveSessionKind, LiveSessions};
use crate::terminal::{
stop_live_agent_runtime_in_project, CloseTerminal, LiveSessionKind, LiveSessions,
};
/// Input for [`AttachLiveAgent::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
@ -223,42 +225,15 @@ impl StopLiveAgent {
project: &Project,
agent_id: AgentId,
) -> Result<StopLiveAgentOutput, AppError> {
// PTY first: delegate to the existing close primitive (removes + kills).
if let Some(session_id) = self
.live
.pty
.session_for_agent_in_project(project.id, &agent_id)
{
self.close
.execute(CloseTerminalInput { session_id })
.await?;
return Ok(StopLiveAgentOutput {
agent_id,
session_id,
kind: LiveSessionKind::Pty,
});
}
// Structured: remove from the registry first (so the uniqueness guard no
// longer sees a live session), then shut the session down out of the lock.
if let Some(session_id) = self
.live
.structured
.session_id_for_agent_in_project(project.id, &agent_id)
{
if let Some(session) = self.live.structured.remove(&session_id) {
session
.shutdown()
.await
.map_err(|e| AppError::Process(e.to_string()))?;
}
return Ok(StopLiveAgentOutput {
agent_id,
session_id,
kind: LiveSessionKind::Structured,
});
}
Err(AppError::NotFound(format!(
"running session for agent {agent_id}"
)))
stop_live_agent_runtime_in_project(project, agent_id, &self.live, &self.close)
.await?
.into_iter()
.next()
.map(|stopped| StopLiveAgentOutput {
agent_id: stopped.agent_id,
session_id: stopped.session_id,
kind: stopped.kind,
})
.ok_or_else(|| AppError::NotFound(format!("running session for agent {agent_id}")))
}
}