//! Agent-level controlled actions over live sessions (Lot D). //! //! 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 //! conversation summaries, the handoff or the provider sessions. use std::sync::Arc; use domain::{AgentId, NodeId, Project, SessionId}; use crate::error::AppError; use crate::terminal::{CloseTerminal, CloseTerminalInput, LiveSessionKind, LiveSessions}; /// Input for [`AttachLiveAgent::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AttachLiveAgentInput { /// The owning project (boundary/symmetry; the registries are global by agent). pub project: Project, /// The already-running agent whose live session is rebound. pub agent_id: AgentId, /// The layout leaf that should host the live session view. pub node_id: NodeId, } /// Output of [`AttachLiveAgent::execute`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct AttachLiveAgentOutput { /// The rebound agent. pub agent_id: AgentId, /// The node now hosting the session view. pub node_id: NodeId, /// The unchanged live session id. pub session_id: SessionId, /// Runtime family that owns the session. pub kind: LiveSessionKind, } /// Rebinds an already-running agent session to a visible layout cell, never /// spawning a process ("a cell is a view"). /// /// Resolves the live session by agent in the PTY registry first, then the /// structured one, and updates only the hosting `node_id` (the PTY handle / agent /// session, its id and scrollback stay intact). Idempotent when the agent is /// already attached to the same node (the rebind is a no-op that returns the same /// session). No log/handoff/provider-session is touched. pub struct AttachLiveAgent { live: Arc, } impl AttachLiveAgent { /// Builds the use case from the aggregated live-session registry. #[must_use] pub fn new(live: Arc) -> Self { Self { live } } /// Rebinds the agent's live session onto `node_id`. /// /// # Errors /// [`AppError::NotFound`] when the agent has no live session in either registry. pub fn execute(&self, input: AttachLiveAgentInput) -> Result { // PTY first, then structured (one-live-session-per-agent ⇒ at most one match). if let Some(session) = self .live .pty .rebind_agent_node(&input.agent_id, input.node_id) { return Ok(AttachLiveAgentOutput { agent_id: input.agent_id, node_id: session.node_id, session_id: session.id, kind: LiveSessionKind::Pty, }); } if let Some(session) = self .live .structured .rebind_agent_node(&input.agent_id, input.node_id) { return Ok(AttachLiveAgentOutput { agent_id: input.agent_id, node_id: input.node_id, session_id: session.id(), kind: LiveSessionKind::Structured, }); } Err(AppError::NotFound(format!( "running session for agent {}", input.agent_id ))) } } /// Input for [`StopLiveAgent::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct StopLiveAgentInput { /// The owning project (boundary/symmetry; the registries are global by agent). pub project: Project, /// The agent whose live session is stopped. pub agent_id: AgentId, } /// Output of [`StopLiveAgent::execute`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct StopLiveAgentOutput { /// The stopped agent. pub agent_id: AgentId, /// The id of the session that was torn down. pub session_id: SessionId, /// Runtime family that owned the session. pub kind: LiveSessionKind, } /// Stops an already-running agent's live session, polymorphically. /// /// 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. pub struct StopLiveAgent { live: Arc, close: Arc, } impl StopLiveAgent { /// Builds the use case from the live-session registry and the close primitive. #[must_use] pub fn new(live: Arc, close: Arc) -> Self { Self { live, close } } /// Tears down the agent's live session. /// /// # Errors /// - [`AppError::NotFound`] when the agent has no live session, /// - [`AppError::Process`] when the PTY kill or structured shutdown fails. pub async fn execute( &self, input: StopLiveAgentInput, ) -> Result { // PTY first: delegate to the existing close primitive (removes + kills). if let Some(session_id) = self.live.pty.session_for_agent(&input.agent_id) { self.close .execute(CloseTerminalInput { session_id }) .await?; return Ok(StopLiveAgentOutput { agent_id: input.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(&input.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: input.agent_id, session_id, kind: LiveSessionKind::Structured, }); } Err(AppError::NotFound(format!( "running session for agent {}", input.agent_id ))) } }