//! Read-only project work-state read model. //! //! This module composes existing runtime state only: the project agent manifest, //! live session registries and the input mediator busy state. It performs no I/O //! beyond loading the manifest through the existing context store and creates no //! durable projection. use std::collections::HashMap; use std::sync::Arc; use domain::input::AgentBusyState; use domain::ports::AgentContextStore; use domain::{AgentId, InputMediator, NodeId, ProfileId, Project, SessionId}; use crate::error::AppError; use crate::terminal::{LiveSessionKind, LiveSessionSnapshot, LiveSessions}; /// Input for [`GetProjectWorkState::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct GetProjectWorkStateInput { /// Project whose manifest provides the agent order and boundary. pub project: Project, } /// Read model for a project's current agent work state. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProjectWorkState { /// Agents in manifest order. pub agents: Vec, } /// Read model for one manifest agent. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AgentWorkState { /// Agent id. pub agent_id: AgentId, /// Agent display name. pub name: String, /// Runtime profile id currently assigned to the agent. pub profile_id: ProfileId, /// Current live session, if the agent is live. pub live: Option, /// Current FIFO/busy state. pub busy: AgentBusyState, } /// Live session coordinates exposed by the work-state read model. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct LiveWorkSession { /// The layout node currently hosting the session view. pub node_id: NodeId, /// The live session id. pub session_id: SessionId, /// Runtime family that owns the session. pub kind: LiveSessionKind, } /// Read-only use case aggregating manifest agents with live and busy state. pub struct GetProjectWorkState { contexts: Arc, live: Arc, input: Arc, } impl GetProjectWorkState { /// Builds the read-model use case from existing stores/registries. #[must_use] pub fn new( contexts: Arc, live: Arc, input: Arc, ) -> Self { Self { contexts, live, input, } } /// Executes the read-only aggregation. /// /// # Errors /// - [`AppError::Store`] when the manifest cannot be loaded, /// - [`AppError::Invalid`] if a manifest entry violates agent invariants. pub async fn execute( &self, input: GetProjectWorkStateInput, ) -> Result { let manifest = self.contexts.load_manifest(&input.project).await?; let live_by_agent = live_by_agent(self.live.live_agent_snapshots()); let agents = manifest .entries .iter() .map(|entry| { let agent = entry .to_agent() .map_err(|err| AppError::Invalid(err.to_string()))?; let live = live_by_agent .get(&agent.id) .map(|snapshot| LiveWorkSession { node_id: snapshot.node_id, session_id: snapshot.session_id, kind: snapshot.kind, }); Ok(AgentWorkState { agent_id: agent.id, name: agent.name, profile_id: agent.profile_id, live, busy: self.input.busy_state(agent.id), }) }) .collect::, AppError>>()?; Ok(ProjectWorkState { agents }) } } fn live_by_agent(snapshots: Vec) -> HashMap { let mut out = HashMap::new(); for snapshot in snapshots { out.entry(snapshot.agent_id).or_insert(snapshot); } out }