fix(custom-cli): le bouton Stop annule le tour courant sans fermer la session et cascade l'annulation aux agents délégués — stop_agent suit l'arbre des dépendances actives (cancel_current_turn + préemption runtime + enfants d'exécution), TurnCancel tue le process du tour en cours, autorisation requester/sous-arbre sur idea_stop_agent, fixtures DTO runtime_state/node_id alignées — #257 (QA verte)
This commit is contained in:
@ -3,8 +3,8 @@
|
||||
//! Companion to the read-only work-state model: these use cases let the UI drive
|
||||
//! an **already-running** agent's live session by **agent id**, never by spawning a
|
||||
//! new process. [`AttachLiveAgent`] rebinds the hosting cell ("a cell is a view");
|
||||
//! [`StopLiveAgent`] tears the live session down polymorphically (PTY kill vs
|
||||
//! structured shutdown). Neither touches the agent entity, its tickets, the
|
||||
//! [`StopLiveAgent`] tears a PTY live session down or cancels the current structured
|
||||
//! turn while preserving the structured session. Neither touches the agent entity, its tickets, the
|
||||
//! conversation summaries, the handoff or the provider sessions.
|
||||
|
||||
use std::sync::Arc;
|
||||
@ -136,14 +136,13 @@ pub trait StopAgentExecutionCascade: Send + Sync {
|
||||
async fn stop_agent_execution_children(&self, project: &Project, agent_id: AgentId);
|
||||
}
|
||||
|
||||
/// Stops an already-running agent's live session, polymorphically.
|
||||
/// Stops an already-running agent's current work.
|
||||
///
|
||||
/// Resolves the session by agent (PTY first, then structured). A PTY session is
|
||||
/// torn down through the existing [`CloseTerminal`] primitive (registry removal +
|
||||
/// process kill); a structured session is removed then `shutdown()`. The agent
|
||||
/// entity, its tickets, conversation summaries and handoff are **not** removed. Any
|
||||
/// exit event is left to the existing infrastructure (the PTY pump / session
|
||||
/// backend) — no bus is invented here.
|
||||
/// Resolves the session by agent (structured first, then PTY). A structured
|
||||
/// session keeps its registry entry and receives `cancel_current_turn()`, so the
|
||||
/// provider conversation/context survives the stop. A PTY session has no finer
|
||||
/// turn boundary, so it is torn down through the existing [`CloseTerminal`]
|
||||
/// primitive (registry removal + process kill).
|
||||
pub struct StopLiveAgent {
|
||||
live: Arc<LiveSessions>,
|
||||
close: Arc<CloseTerminal>,
|
||||
@ -188,7 +187,7 @@ impl StopLiveAgent {
|
||||
self
|
||||
}
|
||||
|
||||
/// Tears down the agent's live session.
|
||||
/// Stops the agent's current work.
|
||||
///
|
||||
/// # Errors
|
||||
/// - [`AppError::NotFound`] when the agent has no live session,
|
||||
@ -197,22 +196,33 @@ impl StopLiveAgent {
|
||||
&self,
|
||||
input: StopLiveAgentInput,
|
||||
) -> Result<StopLiveAgentOutput, AppError> {
|
||||
self.stop_dependencies(&input.project, input.agent_id).await;
|
||||
self.stop_dependencies(&input.project, input.agent_id)
|
||||
.await?;
|
||||
if let Some(mediator) = &self.input {
|
||||
mediator.preempt(RuntimeAgentKey::new(input.project.id, input.agent_id));
|
||||
}
|
||||
if let Some(out) = self
|
||||
.cancel_structured_turn_in_project(&input.project, input.agent_id)
|
||||
.await?
|
||||
{
|
||||
return Ok(out);
|
||||
}
|
||||
self.stop_one_in_project(&input.project, input.agent_id)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn stop_dependencies(&self, project: &Project, agent_id: AgentId) {
|
||||
async fn stop_dependencies(
|
||||
&self,
|
||||
project: &Project,
|
||||
agent_id: AgentId,
|
||||
) -> Result<(), AppError> {
|
||||
if let Some(cascade) = &self.cascade {
|
||||
cascade
|
||||
.stop_agent_execution_children(project, agent_id)
|
||||
.await;
|
||||
}
|
||||
let Some(waits) = &self.waits else {
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
for dep in waits.active_wait_dependencies(agent_id) {
|
||||
if let Some(cascade) = &self.cascade {
|
||||
@ -221,8 +231,52 @@ impl StopLiveAgent {
|
||||
if let Some(mediator) = &self.input {
|
||||
mediator.preempt(RuntimeAgentKey::new(project.id, dep));
|
||||
}
|
||||
let _ = self.stop_one_in_project(project, dep).await;
|
||||
if self
|
||||
.cancel_structured_turn_in_project(project, dep)
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
let _ = self.stop_one_in_project(project, dep).await;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cancel_structured_turn_in_project(
|
||||
&self,
|
||||
project: &Project,
|
||||
agent_id: AgentId,
|
||||
) -> Result<Option<StopLiveAgentOutput>, AppError> {
|
||||
let Some(snapshot) = self
|
||||
.live
|
||||
.live_agent_snapshots_for(project.id, &agent_id)
|
||||
.into_iter()
|
||||
.find(|snapshot| snapshot.kind == LiveSessionKind::Structured)
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(session) = self
|
||||
.live
|
||||
.structured
|
||||
.session_for_agent_in_project(project.id, &agent_id)
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
self.live
|
||||
.mark_runtime_state(snapshot, LiveRuntimeState::Stopping);
|
||||
session
|
||||
.cancel_current_turn()
|
||||
.await
|
||||
.map_err(|err| AppError::Process(err.to_string()))?;
|
||||
self.live
|
||||
.mark_runtime_state(snapshot, LiveRuntimeState::Stopped);
|
||||
Ok(Some(StopLiveAgentOutput {
|
||||
agent_id: snapshot.agent_id,
|
||||
session_id: snapshot.session_id,
|
||||
node_id: snapshot.node_id,
|
||||
kind: snapshot.kind,
|
||||
runtime_state: LiveRuntimeState::Stopped,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn stop_one_in_project(
|
||||
|
||||
Reference in New Issue
Block a user