feat(workstate): read-model live-state minimal des conversations/délégations (Lot A backend)
Introduit le module application `workstate` (modèle de read-model live + snapshots des conversations/délégations en cours) et l'expose via la couche terminal (exports mod/registry). Câble la surface Tauri : DTO, commande et state pour exposer le live-state au frontend (lib + state + commands), avec tests. QA verte. Réserve environnementale non bloquante : tests loopback socket Unix réels non exécutables en sandbox (UnixListener::bind PermissionDenied), alternatives avec skips vertes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -28,6 +28,7 @@ pub mod skill;
|
||||
pub mod template;
|
||||
pub mod terminal;
|
||||
pub mod window;
|
||||
pub mod workstate;
|
||||
|
||||
pub use agent::{
|
||||
drain_with_readiness, drain_with_readiness_outcome, reference_profile_id, reference_profiles,
|
||||
@ -111,8 +112,13 @@ pub use template::{
|
||||
UpdateTemplateOutput,
|
||||
};
|
||||
pub use terminal::{
|
||||
CloseTerminal, CloseTerminalInput, CloseTerminalOutput, LiveAgentRegistry, LiveSessions,
|
||||
OpenTerminal, OpenTerminalInput, OpenTerminalOutput, ResizeTerminal, ResizeTerminalInput,
|
||||
StructuredSessions, TerminalSessions, WriteToTerminal, WriteToTerminalInput,
|
||||
CloseTerminal, CloseTerminalInput, CloseTerminalOutput, LiveAgentRegistry, LiveSessionKind,
|
||||
LiveSessionSnapshot, LiveSessions, OpenTerminal, OpenTerminalInput, OpenTerminalOutput,
|
||||
ResizeTerminal, ResizeTerminalInput, StructuredSessions, TerminalSessions, WriteToTerminal,
|
||||
WriteToTerminalInput,
|
||||
};
|
||||
pub use window::{MoveTabToNewWindow, MoveTabToNewWindowInput, MoveTabToNewWindowOutput};
|
||||
pub use workstate::{
|
||||
AgentWorkState, GetProjectWorkState, GetProjectWorkStateInput, LiveWorkSession,
|
||||
ProjectWorkState,
|
||||
};
|
||||
|
||||
@ -28,7 +28,10 @@
|
||||
mod registry;
|
||||
mod usecases;
|
||||
|
||||
pub use registry::{LiveAgentRegistry, LiveSessions, StructuredSessions, TerminalSessions};
|
||||
pub use registry::{
|
||||
LiveAgentRegistry, LiveSessionKind, LiveSessionSnapshot, LiveSessions, StructuredSessions,
|
||||
TerminalSessions,
|
||||
};
|
||||
pub use usecases::{
|
||||
CloseTerminal, CloseTerminalInput, CloseTerminalOutput, OpenTerminal, OpenTerminalInput,
|
||||
OpenTerminalOutput, ResizeTerminal, ResizeTerminalInput, WriteToTerminal, WriteToTerminalInput,
|
||||
|
||||
@ -13,6 +13,28 @@ use domain::conversation::ConversationId;
|
||||
use domain::ports::{AgentSession, PtyHandle};
|
||||
use domain::{AgentId, NodeId, SessionId, SessionKind, TerminalSession};
|
||||
|
||||
/// Runtime family of a live agent session.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum LiveSessionKind {
|
||||
/// Raw PTY-backed CLI session.
|
||||
Pty,
|
||||
/// Structured agent-session backend.
|
||||
Structured,
|
||||
}
|
||||
|
||||
/// Read-only coordinates of one live agent session.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct LiveSessionSnapshot {
|
||||
/// The agent owning the live session.
|
||||
pub agent_id: AgentId,
|
||||
/// The layout node currently hosting the session view.
|
||||
pub node_id: NodeId,
|
||||
/// The live session id.
|
||||
pub session_id: SessionId,
|
||||
/// Which runtime registry owns the session.
|
||||
pub kind: LiveSessionKind,
|
||||
}
|
||||
|
||||
/// A registered, live terminal: its PTY handle plus the domain snapshot.
|
||||
#[derive(Debug, Clone)]
|
||||
struct Entry {
|
||||
@ -516,6 +538,31 @@ impl LiveSessions {
|
||||
all.extend(self.structured.live_agents());
|
||||
all
|
||||
}
|
||||
|
||||
/// Tous les agents vivants avec le type de registre source (PTY puis structuré).
|
||||
#[must_use]
|
||||
pub fn live_agent_snapshots(&self) -> Vec<LiveSessionSnapshot> {
|
||||
let mut all: Vec<LiveSessionSnapshot> = self
|
||||
.pty
|
||||
.live_agents()
|
||||
.into_iter()
|
||||
.map(|(agent_id, node_id, session_id)| LiveSessionSnapshot {
|
||||
agent_id,
|
||||
node_id,
|
||||
session_id,
|
||||
kind: LiveSessionKind::Pty,
|
||||
})
|
||||
.collect();
|
||||
all.extend(self.structured.live_agents().into_iter().map(
|
||||
|(agent_id, node_id, session_id)| LiveSessionSnapshot {
|
||||
agent_id,
|
||||
node_id,
|
||||
session_id,
|
||||
kind: LiveSessionKind::Structured,
|
||||
},
|
||||
));
|
||||
all
|
||||
}
|
||||
}
|
||||
|
||||
impl LiveAgentRegistry for LiveSessions {
|
||||
|
||||
124
crates/application/src/workstate/mod.rs
Normal file
124
crates/application/src/workstate/mod.rs
Normal file
@ -0,0 +1,124 @@
|
||||
//! 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<AgentWorkState>,
|
||||
}
|
||||
|
||||
/// 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<LiveWorkSession>,
|
||||
/// 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<dyn AgentContextStore>,
|
||||
live: Arc<LiveSessions>,
|
||||
input: Arc<dyn InputMediator>,
|
||||
}
|
||||
|
||||
impl GetProjectWorkState {
|
||||
/// Builds the read-model use case from existing stores/registries.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
contexts: Arc<dyn AgentContextStore>,
|
||||
live: Arc<LiveSessions>,
|
||||
input: Arc<dyn InputMediator>,
|
||||
) -> 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<ProjectWorkState, AppError> {
|
||||
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::<Result<Vec<_>, AppError>>()?;
|
||||
Ok(ProjectWorkState { agents })
|
||||
}
|
||||
}
|
||||
|
||||
fn live_by_agent(snapshots: Vec<LiveSessionSnapshot>) -> HashMap<AgentId, LiveSessionSnapshot> {
|
||||
let mut out = HashMap::new();
|
||||
for snapshot in snapshots {
|
||||
out.entry(snapshot.agent_id).or_insert(snapshot);
|
||||
}
|
||||
out
|
||||
}
|
||||
Reference in New Issue
Block a user