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

@ -47,10 +47,11 @@ use domain::{PtySize, SessionId, SkillId, SkillRef};
use uuid::Uuid;
use application::{
CreateAgentFromScratch, CreateAgentInput, DeleteAgent, DeleteAgentInput, LaunchAgent,
LaunchAgentInput, ListAgents, ListAgentsInput, PermissionProjectorRegistry, ReadAgentContext,
ReadAgentContextInput, StructuredRoutingMode, StructuredSessions, TerminalSessions,
UpdateAgentContext, UpdateAgentContextInput, UpdateAgentEffort, UpdateAgentEffortInput,
CloseTerminal, CreateAgentFromScratch, CreateAgentInput, DeleteAgent, DeleteAgentInput,
LaunchAgent, LaunchAgentInput, ListAgents, ListAgentsInput, LiveSessions,
PermissionProjectorRegistry, ReadAgentContext, ReadAgentContextInput, StructuredRoutingMode,
StructuredSessions, TerminalSessions, UpdateAgentContext, UpdateAgentContextInput,
UpdateAgentEffort, UpdateAgentEffortInput,
};
// ---------------------------------------------------------------------------
@ -585,6 +586,7 @@ impl PtyPort for FakePty {
struct FakeSession {
id: SessionId,
conversation_id: Option<String>,
shutdowns: Arc<Mutex<Vec<SessionId>>>,
}
#[async_trait]
@ -602,6 +604,7 @@ impl AgentSession for FakeSession {
}
async fn shutdown(&self) -> Result<(), AgentSessionError> {
self.shutdowns.lock().unwrap().push(self.id);
Ok(())
}
}
@ -613,6 +616,7 @@ struct FakeStructuredFactory {
efforts: Arc<Mutex<Vec<Option<String>>>>,
envs: Arc<Mutex<Vec<Vec<(String, String)>>>>,
policies: Arc<Mutex<Vec<Option<domain::ports::StructuredProviderLaunchPolicy>>>>,
shutdowns: Arc<Mutex<Vec<SessionId>>>,
next_session: SessionId,
}
@ -624,6 +628,7 @@ impl FakeStructuredFactory {
efforts: Arc::new(Mutex::new(Vec::new())),
envs: Arc::new(Mutex::new(Vec::new())),
policies: Arc::new(Mutex::new(Vec::new())),
shutdowns: Arc::new(Mutex::new(Vec::new())),
next_session,
}
}
@ -679,6 +684,7 @@ impl AgentSessionFactory for FakeStructuredFactory {
Ok(Arc::new(FakeSession {
id: self.next_session,
conversation_id: Some("engine-session-1".to_owned()),
shutdowns: Arc::clone(&self.shutdowns),
}))
}
}
@ -1048,6 +1054,56 @@ async fn delete_removes_entry_then_unknown_is_not_found() {
assert_eq!(err.code(), "NOT_FOUND", "got {err:?}");
}
#[tokio::test]
async fn delete_agent_cleans_structured_then_pty_runtime_before_manifest_removal() {
let a = scratch_agent(aid(1), "Backend", "agents/backend.md", pid(9));
let contexts = FakeContexts::with_agent(&a, "ctx");
let sessions = Arc::new(TerminalSessions::new());
let structured = Arc::new(StructuredSessions::new());
let live = Arc::new(LiveSessions::new(
Arc::clone(&sessions),
Arc::clone(&structured),
));
let pty = FakePty::new(trace(), sid(42));
let close = Arc::new(CloseTerminal::new(
Arc::new(pty.clone()),
Arc::clone(&sessions),
));
let shutdowns = Arc::new(Mutex::new(Vec::new()));
structured.insert_in_project(
project().id,
Arc::new(FakeSession {
id: sid(555),
conversation_id: Some("engine-session-stale".to_owned()),
shutdowns: Arc::clone(&shutdowns),
}),
a.id,
nid(7),
);
seed_live_agent_session(&sessions, a.id, nid(8), sid(42));
let delete = DeleteAgent::new(Arc::new(contexts.clone()), Arc::new(SpyBus::default()))
.with_runtime_cleanup(live, close);
delete
.execute(DeleteAgentInput {
project: project(),
agent_id: a.id,
})
.await
.unwrap();
assert_eq!(*shutdowns.lock().unwrap(), vec![sid(555)]);
assert_eq!(pty.kills(), vec![sid(42)]);
assert!(structured
.session_for_agent_in_project(project().id, &a.id)
.is_none());
assert!(sessions
.session_for_agent_in_project(project().id, &a.id)
.is_none());
assert!(contexts.manifest().entries.is_empty());
}
// ---------------------------------------------------------------------------
// LaunchAgent
// ---------------------------------------------------------------------------
@ -1369,6 +1425,7 @@ async fn chat_intent_reattaches_existing_structured_session_after_stale_pty_tran
Arc::new(FakeSession {
id: sid(555),
conversation_id: Some("engine-session-stale".to_owned()),
shutdowns: Arc::new(Mutex::new(Vec::new())),
}),
agent.id,
stale_custom_node,