feat(background): livraison auto au propriétaire + projection des tâches de fond dans le work-state (T1+T3)
T1 — Wake automatique du propriétaire à la complétion. La complétion d'une tâche de fond est désormais livrée à l'agent propriétaire dès que la session accepte l'envoi (wake.rs : mark_completion_delivered au send accepté), via un drain de flux dédié (structured.rs : drain_reply_stream_with_readiness). L'inbox médiée enfile l'item sans démarrer de tour ni marquer l'agent busy (input/mod.rs : enqueue FIFO silencieux). Régression couverte (tests/agent_wake.rs, tests input/mod.rs). T3 — Tâches de fond projetées dans le read-model du panneau Work. AgentWorkState porte désormais background_tasks (VO AgentBackgroundTaskState), alimenté par un builder best-effort with_background_tasks(store) : union list_open_for_agent + dispatch des completions non livrées par owner_agent_id, erreur store => Vec vide (aucune régression live/busy/tickets). DTO Tauri backgroundTasks et wiring du BackgroundTaskStore côté state.rs. Le frontend, déjà câblé, affiche Cancel/Retry (mapping queued/waiting -> pending, tri sur updatedAtMs). Borne V1 : une tâche terminale déjà livrée n'est plus énumérable (Retry limité à la fenêtre non livrée). Tests : cargo build --workspace OK ; cargo test -p application / -p app-tauri / -p infrastructure verts ; frontend build + vitest verts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -31,28 +31,27 @@ use crate::dto::{
|
||||
parse_agent_id, parse_close_terminal, parse_delete_profile, parse_layout_id, parse_memory_slug,
|
||||
parse_node_id, parse_profile_id, parse_project_id, parse_session_id, parse_skill_id,
|
||||
parse_task_id, parse_template_id, parse_ticket_id, AgentDriftListDto, AgentDto, AgentListDto,
|
||||
BackgroundTaskDto,
|
||||
AssignSkillRequestDto, AttachLiveAgentRequestDto, AttachLiveAgentResponseDto,
|
||||
ChangeAgentProfileDto, ChangeAgentProfileRequestDto, ConfigureProfilesRequestDto,
|
||||
ConversationDetailsDto, CreateAgentFromTemplateRequestDto, CreateAgentRequestDto,
|
||||
CreateLayoutRequestDto, CreateLayoutResultDto, CreateMemoryRequestDto, CreateProjectRequestDto,
|
||||
CreateSkillRequestDto, CreateTemplateRequestDto, DeleteLayoutRequestDto, DeleteLayoutResultDto,
|
||||
DeliveredDelegationRequestDto, DetectProfilesRequestDto, DetectProfilesResponseDto,
|
||||
EffectivePermissionsDto, EmbedderEnginesDto, EmbedderProfileDto, EmbedderProfileListDto,
|
||||
ErrorDto, FirstRunStateDto, FrontAttachedRequestDto, GitBranchesDto, GitCheckoutRequestDto,
|
||||
GitCommitDto, GitCommitListDto, GitCommitRequestDto, GitStageRequestDto, GitStatusListDto,
|
||||
GraphCommitListDto, HealthRequestDto, HealthResponseDto, InspectConversationRequestDto,
|
||||
InterruptAgentRequestDto, LaunchAgentRequestDto, LayoutDto, LayoutOperationDto, ListLayoutsDto,
|
||||
LiveAgentListDto, MemoryDto, MemoryIndexDto, MemoryLinksDto, MemoryListDto,
|
||||
OpenTerminalRequestDto, ProfileDto, ProfileListDto, ProjectDto, ProjectListDto,
|
||||
ProjectPermissionsDto, ProjectWorkStateDto, ReadAgentContextResponseDto,
|
||||
ReadConversationPageRequestDto, ReattachChatDto, ReattachResultDto, RecallMemoryRequestDto,
|
||||
RenameLayoutRequestDto, ReplyChunk, ResizeTerminalRequestDto,
|
||||
ResolveAgentPermissionsRequestDto, ResumableAgentListDto, SaveEmbedderProfileRequestDto,
|
||||
SaveProfileRequestDto, SetActiveLayoutRequestDto, SetActiveLayoutResultDto, SkillDto,
|
||||
SkillListDto, StopLiveAgentRequestDto, StopLiveAgentResponseDto,
|
||||
SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto, TemplateListDto,
|
||||
TerminalClosedDto, TerminalSessionDto, TurnPageDto, UnassignSkillRequestDto,
|
||||
BackgroundTaskDto, ChangeAgentProfileDto, ChangeAgentProfileRequestDto,
|
||||
ConfigureProfilesRequestDto, ConversationDetailsDto, CreateAgentFromTemplateRequestDto,
|
||||
CreateAgentRequestDto, CreateLayoutRequestDto, CreateLayoutResultDto, CreateMemoryRequestDto,
|
||||
CreateProjectRequestDto, CreateSkillRequestDto, CreateTemplateRequestDto,
|
||||
DeleteLayoutRequestDto, DeleteLayoutResultDto, DeliveredDelegationRequestDto,
|
||||
DetectProfilesRequestDto, DetectProfilesResponseDto, EffectivePermissionsDto,
|
||||
EmbedderEnginesDto, EmbedderProfileDto, EmbedderProfileListDto, ErrorDto, FirstRunStateDto,
|
||||
FrontAttachedRequestDto, GitBranchesDto, GitCheckoutRequestDto, GitCommitDto, GitCommitListDto,
|
||||
GitCommitRequestDto, GitStageRequestDto, GitStatusListDto, GraphCommitListDto,
|
||||
HealthRequestDto, HealthResponseDto, InspectConversationRequestDto, InterruptAgentRequestDto,
|
||||
LaunchAgentRequestDto, LayoutDto, LayoutOperationDto, ListLayoutsDto, LiveAgentListDto,
|
||||
MemoryDto, MemoryIndexDto, MemoryLinksDto, MemoryListDto, OpenTerminalRequestDto, ProfileDto,
|
||||
ProfileListDto, ProjectDto, ProjectListDto, ProjectPermissionsDto, ProjectWorkStateDto,
|
||||
ReadAgentContextResponseDto, ReadConversationPageRequestDto, ReattachChatDto,
|
||||
ReattachResultDto, RecallMemoryRequestDto, RenameLayoutRequestDto, ReplyChunk,
|
||||
ResizeTerminalRequestDto, ResolveAgentPermissionsRequestDto, ResumableAgentListDto,
|
||||
SaveEmbedderProfileRequestDto, SaveProfileRequestDto, SetActiveLayoutRequestDto,
|
||||
SetActiveLayoutResultDto, SkillDto, SkillListDto, StopLiveAgentRequestDto,
|
||||
StopLiveAgentResponseDto, SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto,
|
||||
TemplateListDto, TerminalClosedDto, TerminalSessionDto, TurnPageDto, UnassignSkillRequestDto,
|
||||
UpdateAgentContextRequestDto, UpdateAgentPermissionsRequestDto, UpdateMemoryRequestDto,
|
||||
UpdateProjectContextRequestDto, UpdateProjectPermissionsRequestDto, UpdateSkillRequestDto,
|
||||
UpdateTemplateRequestDto, WriteTerminalRequestDto,
|
||||
|
||||
@ -9,11 +9,12 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use application::{
|
||||
AgentTicketState, AppError, AttachLiveAgentOutput, ConversationPreviewStatus,
|
||||
ConversationTurnWorkPreview, ConversationWorkSummary, CreateProjectInput, CreateProjectOutput,
|
||||
GitGraphOutput, HealthInput, HealthReport, LayoutKind, ListProjectsOutput, LiveSessionKind,
|
||||
LiveSessionSnapshot, OpenProjectOutput, ProjectWorkState, StopLiveAgentOutput,
|
||||
TicketWorkSource, TicketWorkStatus, TurnPage, TurnSource, TurnView,
|
||||
AgentBackgroundTaskState, AgentTicketState, AppError, AttachLiveAgentOutput,
|
||||
BackgroundTaskKindLabel, ConversationPreviewStatus, ConversationTurnWorkPreview,
|
||||
ConversationWorkSummary, CreateProjectInput, CreateProjectOutput, GitGraphOutput, HealthInput,
|
||||
HealthReport, LayoutKind, ListProjectsOutput, LiveSessionKind, LiveSessionSnapshot,
|
||||
OpenProjectOutput, ProjectWorkState, StopLiveAgentOutput, TicketWorkSource, TicketWorkStatus,
|
||||
TurnPage, TurnSource, TurnView,
|
||||
};
|
||||
use domain::{AgentBusyState, PageCursor, PageDirection, Project, ProjectId, TurnRole};
|
||||
|
||||
@ -1607,6 +1608,50 @@ impl From<AgentTicketState> for AgentTicketStateDto {
|
||||
}
|
||||
}
|
||||
|
||||
/// One background task owned by an agent in the Work panel read model.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AgentBackgroundTaskStateDto {
|
||||
/// Stable task id (UUID string).
|
||||
pub task_id: String,
|
||||
/// Kind discriminant.
|
||||
pub kind: String,
|
||||
/// Lifecycle state.
|
||||
pub state: String,
|
||||
/// Process exit code, when the terminal result carries one.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub exit_code: Option<i32>,
|
||||
/// Human-readable summary / error / reason of the terminal result.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub summary: Option<String>,
|
||||
/// Bounded stdout tail.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stdout_tail: Option<String>,
|
||||
/// Bounded stderr tail.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stderr_tail: Option<String>,
|
||||
/// Creation timestamp, epoch milliseconds.
|
||||
pub created_at_ms: u64,
|
||||
/// Last update timestamp, epoch milliseconds.
|
||||
pub updated_at_ms: u64,
|
||||
}
|
||||
|
||||
impl From<AgentBackgroundTaskState> for AgentBackgroundTaskStateDto {
|
||||
fn from(task: AgentBackgroundTaskState) -> Self {
|
||||
Self {
|
||||
task_id: task.task_id.to_string(),
|
||||
kind: background_kind_label_from_work_state(task.kind).to_owned(),
|
||||
state: background_state_label(task.state).to_owned(),
|
||||
exit_code: task.exit_code,
|
||||
summary: task.summary,
|
||||
stdout_tail: task.stdout_tail,
|
||||
stderr_tail: task.stderr_tail,
|
||||
created_at_ms: task.created_at_ms,
|
||||
updated_at_ms: task.updated_at_ms,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One manifest agent's current live/busy state.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@ -1623,6 +1668,8 @@ pub struct AgentWorkStateDto {
|
||||
pub busy: AgentBusyState,
|
||||
/// Pending/in-progress delegation tickets, in FIFO order.
|
||||
pub tickets: Vec<AgentTicketStateDto>,
|
||||
/// Best-effort first-class background tasks owned by this agent.
|
||||
pub background_tasks: Vec<AgentBackgroundTaskStateDto>,
|
||||
}
|
||||
|
||||
/// How much of a [`ConversationWorkSummaryDto`] could be derived, best-effort.
|
||||
@ -1748,6 +1795,11 @@ impl From<ProjectWorkState> for ProjectWorkStateDto {
|
||||
.into_iter()
|
||||
.map(AgentTicketStateDto::from)
|
||||
.collect(),
|
||||
background_tasks: agent
|
||||
.background_tasks
|
||||
.into_iter()
|
||||
.map(AgentBackgroundTaskStateDto::from)
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
conversations: state
|
||||
@ -2924,6 +2976,16 @@ fn background_kind_label(kind: &BackgroundTaskKind) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// Kind discriminant string for a work-state [`BackgroundTaskKindLabel`].
|
||||
fn background_kind_label_from_work_state(kind: BackgroundTaskKindLabel) -> &'static str {
|
||||
match kind {
|
||||
BackgroundTaskKindLabel::Command => "command",
|
||||
BackgroundTaskKindLabel::HeadlessRendezvous => "headlessRendezvous",
|
||||
BackgroundTaskKindLabel::SessionResume => "sessionResume",
|
||||
BackgroundTaskKindLabel::Maintenance => "maintenance",
|
||||
}
|
||||
}
|
||||
|
||||
/// Lifecycle state string for a [`BackgroundTaskState`].
|
||||
fn background_state_label(state: BackgroundTaskState) -> &'static str {
|
||||
match state {
|
||||
|
||||
@ -503,6 +503,48 @@ impl AppReconcileBackgroundTasks {
|
||||
}
|
||||
}
|
||||
|
||||
fn schedule_background_ready_retry(
|
||||
ready_tx: tokio::sync::mpsc::UnboundedSender<BackgroundTaskReadyToDeliver>,
|
||||
ready: BackgroundTaskReadyToDeliver,
|
||||
) {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
let _ = ready_tx.send(ready);
|
||||
});
|
||||
}
|
||||
|
||||
fn schedule_background_wake_retry(
|
||||
wake: Arc<dyn AgentWakePort>,
|
||||
project: Project,
|
||||
owner_agent_id: AgentId,
|
||||
task_id: TaskId,
|
||||
) {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
match wake
|
||||
.wake_agent(
|
||||
&project,
|
||||
owner_agent_id,
|
||||
WakeReason::BackgroundCompletion { task_id },
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => break,
|
||||
Err(WakeError::AgentBusy { .. }) => continue,
|
||||
Err(err) => {
|
||||
application::diag!(
|
||||
"[background-task] completion wake retry failed: task={} owner={} err={err}",
|
||||
task_id,
|
||||
owner_agent_id
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
struct AppWakeSessionProvider {
|
||||
launch_agent: Arc<LaunchAgent>,
|
||||
structured_sessions: Arc<StructuredSessions>,
|
||||
@ -1690,6 +1732,7 @@ impl AppState {
|
||||
let wake = Arc::clone(&background_wake);
|
||||
let projects = Arc::clone(&store_port);
|
||||
let clock_for_items = Arc::clone(&clock) as Arc<dyn Clock>;
|
||||
let retry_ready = background_ready_tx.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
while let Some(ready) = background_ready_rx.recv().await {
|
||||
let item = InboxItem {
|
||||
@ -1712,6 +1755,7 @@ impl AppState {
|
||||
ready.owner_agent_id,
|
||||
receipt.depth
|
||||
);
|
||||
schedule_background_ready_retry(retry_ready.clone(), ready);
|
||||
continue;
|
||||
}
|
||||
Ok(_) => {}
|
||||
@ -1722,6 +1766,7 @@ impl AppState {
|
||||
ready.task_id,
|
||||
ready.owner_agent_id
|
||||
);
|
||||
schedule_background_ready_retry(retry_ready.clone(), ready);
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
@ -1758,6 +1803,20 @@ impl AppState {
|
||||
)
|
||||
.await
|
||||
{
|
||||
if matches!(err, WakeError::AgentBusy { .. }) {
|
||||
application::diag!(
|
||||
"[background-task] completion wake postponed: task={} owner={}",
|
||||
ready.task_id,
|
||||
ready.owner_agent_id
|
||||
);
|
||||
schedule_background_wake_retry(
|
||||
Arc::clone(&wake),
|
||||
project,
|
||||
ready.owner_agent_id,
|
||||
ready.task_id,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
application::diag!(
|
||||
"[background-task] completion wake failed: task={} owner={} err={err}",
|
||||
ready.task_id,
|
||||
@ -1801,7 +1860,8 @@ impl AppState {
|
||||
Arc::new(AppHandoffProvider) as Arc<dyn application::HandoffProvider>,
|
||||
Arc::new(AppConversationLogProvider)
|
||||
as Arc<dyn application::ConversationLogProvider>,
|
||||
),
|
||||
)
|
||||
.with_background_tasks(Arc::clone(&background_tasks_port)),
|
||||
);
|
||||
// Lot LS6 — rotation (hors chemin chaud) + lecture humaine paginée. Tous deux
|
||||
// composent le provider d'archive par root ; la rotation lit aussi le handoff
|
||||
|
||||
Reference in New Issue
Block a user