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:
2026-06-20 18:06:22 +02:00
parent 6cfa0b04c6
commit aae18499a9
10 changed files with 663 additions and 28 deletions

View File

@ -12,13 +12,13 @@ use application::{
AppError, AssignSkillToAgentInput, ChangeAgentProfileInput, CloseProjectInput,
CreateAgentInput, CreateLayoutInput, CreateMemoryInput, CreateSkillInput, DeleteAgentInput,
DeleteEmbedderProfileInput, DeleteLayoutInput, DeleteMemoryInput, DeleteSkillInput,
DeleteTemplateInput, DetectAgentDriftInput, GetMemoryInput, GitBranchesInput, GitCheckoutInput,
GitCommitInput, GitGraphInput, GitInitInput, GitLogInput, GitStagePathInput, GitStatusInput,
InspectConversationInput, LaunchAgentInput, ListAgentsInput, ListLayoutsInput,
ListMemoriesInput, ListResumableAgentsInput, ListSkillsInput, LiveSessions, LoadLayoutInput,
McpRuntime, MutateLayoutInput, OpenProjectInput, ReadAgentContextInput, ReadMemoryIndexInput,
ReadProjectContextInput, RecallMemoryInput, ReconcileLayoutsInput, RenameLayoutInput,
ResolveAgentPermissionsInput, ResolveMemoryLinksInput, SetActiveLayoutInput,
DeleteTemplateInput, DetectAgentDriftInput, GetMemoryInput, GetProjectWorkStateInput,
GitBranchesInput, GitCheckoutInput, GitCommitInput, GitGraphInput, GitInitInput, GitLogInput,
GitStagePathInput, GitStatusInput, InspectConversationInput, LaunchAgentInput, ListAgentsInput,
ListLayoutsInput, ListMemoriesInput, ListResumableAgentsInput, ListSkillsInput, LiveSessions,
LoadLayoutInput, McpRuntime, MutateLayoutInput, OpenProjectInput, ReadAgentContextInput,
ReadMemoryIndexInput, ReadProjectContextInput, RecallMemoryInput, ReconcileLayoutsInput,
RenameLayoutInput, ResolveAgentPermissionsInput, ResolveMemoryLinksInput, SetActiveLayoutInput,
SnapshotRunningAgentsInput, SyncAgentWithTemplateInput, UnassignSkillFromAgentInput,
UpdateAgentContextInput, UpdateAgentPermissionsInput, UpdateMemoryInput,
UpdateProjectContextInput, UpdateProjectPermissionsInput, UpdateSkillInput,
@ -42,11 +42,11 @@ use crate::dto::{
InterruptAgentRequestDto, LaunchAgentRequestDto, LayoutDto, LayoutOperationDto, ListLayoutsDto,
LiveAgentListDto, MemoryDto, MemoryIndexDto, MemoryLinksDto, MemoryListDto,
OpenTerminalRequestDto, ProfileDto, ProfileListDto, ProjectDto, ProjectListDto,
ProjectPermissionsDto, ReadAgentContextResponseDto, ReattachChatDto, ReattachResultDto,
RecallMemoryRequestDto, RenameLayoutRequestDto, ReplyChunk, ResizeTerminalRequestDto,
ResolveAgentPermissionsRequestDto, ResumableAgentListDto, SaveEmbedderProfileRequestDto,
SaveProfileRequestDto, SetActiveLayoutRequestDto, SkillDto, SkillListDto,
SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto, TemplateListDto,
ProjectPermissionsDto, ProjectWorkStateDto, ReadAgentContextResponseDto, ReattachChatDto,
ReattachResultDto, RecallMemoryRequestDto, RenameLayoutRequestDto, ReplyChunk,
ResizeTerminalRequestDto, ResolveAgentPermissionsRequestDto, ResumableAgentListDto,
SaveEmbedderProfileRequestDto, SaveProfileRequestDto, SetActiveLayoutRequestDto, SkillDto,
SkillListDto, SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto, TemplateListDto,
TerminalClosedDto, TerminalSessionDto, UnassignSkillRequestDto, UpdateAgentContextRequestDto,
UpdateAgentPermissionsRequestDto, UpdateMemoryRequestDto, UpdateProjectContextRequestDto,
UpdateProjectPermissionsRequestDto, UpdateSkillRequestDto, UpdateTemplateRequestDto,
@ -968,6 +968,25 @@ pub async fn list_agents(
.map_err(ErrorDto::from)
}
/// `get_project_work_state` — read-only live/busy state for manifest agents.
///
/// # Errors
/// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the
/// project is unknown, `STORE` on manifest I/O failure).
#[tauri::command]
pub async fn get_project_work_state(
project_id: String,
state: State<'_, AppState>,
) -> Result<ProjectWorkStateDto, ErrorDto> {
let project = resolve_project(&project_id, &state).await?;
state
.get_project_work_state
.execute(GetProjectWorkStateInput { project })
.await
.map(ProjectWorkStateDto::from)
.map_err(ErrorDto::from)
}
/// `list_live_agents` — list every agent that currently owns a live session
/// (raw PTY **or** structured/chat) and the cell hosting each, so the UI can
/// disable an agent already running in another cell (the "one live session per

View File

@ -10,9 +10,9 @@ use serde::{Deserialize, Serialize};
use application::{
AppError, CreateProjectInput, CreateProjectOutput, GitGraphOutput, HealthInput, HealthReport,
LayoutKind, ListProjectsOutput, OpenProjectOutput,
LayoutKind, ListProjectsOutput, LiveSessionKind, OpenProjectOutput, ProjectWorkState,
};
use domain::{Project, ProjectId};
use domain::{AgentBusyState, Project, ProjectId};
/// Request DTO for the `health` command.
#[derive(Debug, Clone, Default, Deserialize)]
@ -1472,6 +1472,83 @@ impl LiveAgentListDto {
}
}
/// Runtime family of a live work-state session.
#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum LiveWorkSessionKindDto {
/// Raw PTY-backed CLI session.
Pty,
/// Structured agent-session backend.
Structured,
}
impl From<LiveSessionKind> for LiveWorkSessionKindDto {
fn from(kind: LiveSessionKind) -> Self {
match kind {
LiveSessionKind::Pty => Self::Pty,
LiveSessionKind::Structured => Self::Structured,
}
}
}
/// Live session coordinates in the project work-state read model.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LiveWorkSessionDto {
/// The layout node currently hosting the session view.
pub node_id: String,
/// The live session id.
pub session_id: String,
/// Runtime family that owns the session.
pub kind: LiveWorkSessionKindDto,
}
/// One manifest agent's current live/busy state.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentWorkStateDto {
/// Agent id.
pub agent_id: String,
/// Agent display name.
pub name: String,
/// Runtime profile id assigned to the agent.
pub profile_id: String,
/// Live session, if any.
pub live: Option<LiveWorkSessionDto>,
/// Current mediated-input busy state.
pub busy: AgentBusyState,
}
/// Project-level read model for conversation/delegation UX.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProjectWorkStateDto {
/// Manifest agents in manifest order, enriched with live/busy state.
pub agents: Vec<AgentWorkStateDto>,
}
impl From<ProjectWorkState> for ProjectWorkStateDto {
fn from(state: ProjectWorkState) -> Self {
Self {
agents: state
.agents
.into_iter()
.map(|agent| AgentWorkStateDto {
agent_id: agent.agent_id.to_string(),
name: agent.name,
profile_id: agent.profile_id.to_string(),
live: agent.live.map(|live| LiveWorkSessionDto {
node_id: live.node_id.to_string(),
session_id: live.session_id.to_string(),
kind: live.kind.into(),
}),
busy: agent.busy,
})
.collect(),
}
}
}
/// Request DTO for `attach_live_agent`: bind an already-running agent session to
/// a visible layout cell without spawning a new process.
#[derive(Debug, Clone, Deserialize)]

View File

@ -163,6 +163,7 @@ pub fn run() {
commands::dismiss_embedder_suggestion,
commands::create_agent,
commands::list_agents,
commands::get_project_work_state,
commands::list_live_agents,
commands::attach_live_agent,
commands::read_agent_context,

View File

@ -18,15 +18,15 @@ use application::{
CreateSkill, CreateTemplate, DeleteAgent, DeleteEmbedderProfile, DeleteLayout, DeleteMemory,
DeleteProfile, DeleteSkill, DeleteTemplate, DescribeEmbedderEngines, DetectAgentDrift,
DetectProfiles, DismissEmbedderSuggestion, FirstRunState, GetMemory, GetProjectPermissions,
GitBranches, GitCheckout, GitCommit, GitGraph, GitInit, GitLog, GitStage, GitStatus,
GitUnstage, HealthUseCase, InspectConversation, LaunchAgent, LaunchAgentInput, ListAgents,
ListAgentsInput, ListEmbedderProfiles, ListLayouts, ListMemories, ListProfiles, ListProjects,
ListResumableAgents, ListSkills, ListTemplates, LiveAgentRegistry, LoadLayout, McpRuntime,
MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject, OpenTerminal,
OrchestratorService, PermissionProjectorRegistry, ProposeContext, ReadAgentContext,
ReadContext, ReadMemory, ReadMemoryIndex, ReadProjectContext, RecallMemory, ReconcileLayouts,
RecordTurn, RecordTurnProvider, ReferenceProfiles, RenameLayout, ResizeTerminal,
ResolveAgentPermissions, ResolveMemoryLinks, SaveEmbedderProfile, SaveProfile,
GetProjectWorkState, GitBranches, GitCheckout, GitCommit, GitGraph, GitInit, GitLog, GitStage,
GitStatus, GitUnstage, HealthUseCase, InspectConversation, LaunchAgent, LaunchAgentInput,
ListAgents, ListAgentsInput, ListEmbedderProfiles, ListLayouts, ListMemories, ListProfiles,
ListProjects, ListResumableAgents, ListSkills, ListTemplates, LiveAgentRegistry, LiveSessions,
LoadLayout, McpRuntime, MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject,
OpenTerminal, OrchestratorService, PermissionProjectorRegistry, ProposeContext,
ReadAgentContext, ReadContext, ReadMemory, ReadMemoryIndex, ReadProjectContext, RecallMemory,
ReconcileLayouts, RecordTurn, RecordTurnProvider, ReferenceProfiles, RenameLayout,
ResizeTerminal, ResolveAgentPermissions, ResolveMemoryLinks, SaveEmbedderProfile, SaveProfile,
SessionLimitService, SetActiveLayout, SnapshotRunningAgents, StructuredSessions,
SuggestedThisSession, SyncAgentWithTemplate, TerminalSessions, UnassignSkillFromAgent,
UpdateAgentContext, UpdateAgentPermissions, UpdateMemory, UpdateProjectContext,
@ -330,6 +330,8 @@ pub struct AppState {
/// Read-only inventory of a project's resumable agent cells, for the reopen
/// panel (§15.2).
pub list_resumable_agents: Arc<ListResumableAgents>,
/// Read-only live/busy state for the project's manifest agents.
pub get_project_work_state: Arc<GetProjectWorkState>,
/// Best-effort inspection of a conversation (last topic + token indicator)
/// for the resume popup (T7). Optional/extensible: backed by a `Vec` of
/// [`domain::ports::SessionInspector`]s; an empty/missing match yields empty
@ -1021,6 +1023,15 @@ impl AppState {
});
}
let input_mediator = Arc::clone(&mediated_inbox) as Arc<dyn domain::input::InputMediator>;
let live_sessions = Arc::new(LiveSessions::new(
Arc::clone(&terminal_sessions),
Arc::clone(&structured_sessions),
));
let get_project_work_state = Arc::new(GetProjectWorkState::new(
Arc::clone(&contexts_port),
Arc::clone(&live_sessions),
Arc::clone(&input_mediator),
));
// --- Limites de session des agents (ARCHITECTURE §21, LS7) ---
// Service pur-ports « détecter → planifier → reprendre » câblé sur l'existant :
@ -1187,6 +1198,7 @@ impl AppState {
launch_agent,
change_agent_profile,
list_resumable_agents,
get_project_work_state,
inspect_conversation,
project_store,
get_project_permissions,