feat(background): runner PTY B8 + boucle sink fermée + refactor point-2
Livre le lot backend B8 des tâches de fond : - infrastructure : runner de commandes concret (CommandBackgroundRunner) sur le port BackgroundTaskRunner, tail borné (bounded_tail/BoundedTail), éclatement du module background_task en sous-modules (mod/runner/tail, sink extrait de l'ancien background_task.rs). - application : nouveau module background exposant les cas d'usage SpawnBackgroundCommand, CancelBackgroundTask, RetryBackgroundTask et le port BackgroundCommandArchive. - domain : refactor point-2 de l'arbitrage Architect — sortie du trait BackgroundCommandArchive de la couche domaine vers application. - app-tauri : câblage runtime (commands, dto, state, lib) des commandes spawn/cancel/retry et de la boucle de complétion sink fermée en composition root. Build workspace + tests application/infrastructure verts (QA). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -2866,3 +2866,167 @@ mod ls6_pagination_tests {
|
||||
assert!(dto.turns.is_empty() && !dto.has_more && dto.next_anchor.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Background tasks (B8 — first-class background command)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
use domain::{
|
||||
BackgroundTask, BackgroundTaskKind, BackgroundTaskResult, BackgroundTaskState, TaskId,
|
||||
};
|
||||
|
||||
/// One first-class background task as seen by the frontend (camelCase wire shape).
|
||||
///
|
||||
/// Mirrors the persisted [`BackgroundTask`], flattening the terminal
|
||||
/// [`BackgroundTaskResult`] into optional `exitCode` / `summary` / tails so the
|
||||
/// UI has a single shape for every lifecycle state. Optional fields are omitted
|
||||
/// from the wire when absent (`skip_serializing_if`).
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BackgroundTaskDto {
|
||||
/// Stable task id (UUID string).
|
||||
pub task_id: String,
|
||||
/// Owning agent id (UUID string).
|
||||
pub owner_agent_id: String,
|
||||
/// Owning project id (UUID string).
|
||||
pub project_id: String,
|
||||
/// Kind discriminant (`"command"`, `"headlessRendezvous"`, `"sessionResume"`,
|
||||
/// `"maintenance"`).
|
||||
pub kind: String,
|
||||
/// Lifecycle state (`"queued"`, `"running"`, `"waiting"`, `"completed"`,
|
||||
/// `"failed"`, `"cancelled"`, `"expired"`).
|
||||
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 (merged output for PTY-backed commands).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stdout_tail: Option<String>,
|
||||
/// Bounded stderr tail (unset for PTY-backed commands, which merge streams).
|
||||
#[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,
|
||||
}
|
||||
|
||||
/// Kind discriminant string for a [`BackgroundTaskKind`].
|
||||
fn background_kind_label(kind: &BackgroundTaskKind) -> &'static str {
|
||||
match kind {
|
||||
BackgroundTaskKind::Command { .. } => "command",
|
||||
BackgroundTaskKind::HeadlessRendezvous { .. } => "headlessRendezvous",
|
||||
BackgroundTaskKind::SessionResume { .. } => "sessionResume",
|
||||
BackgroundTaskKind::Maintenance { .. } => "maintenance",
|
||||
}
|
||||
}
|
||||
|
||||
/// Lifecycle state string for a [`BackgroundTaskState`].
|
||||
fn background_state_label(state: BackgroundTaskState) -> &'static str {
|
||||
match state {
|
||||
BackgroundTaskState::Queued => "queued",
|
||||
BackgroundTaskState::Running => "running",
|
||||
BackgroundTaskState::Waiting => "waiting",
|
||||
BackgroundTaskState::Completed => "completed",
|
||||
BackgroundTaskState::Failed => "failed",
|
||||
BackgroundTaskState::Cancelled => "cancelled",
|
||||
BackgroundTaskState::Expired => "expired",
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BackgroundTask> for BackgroundTaskDto {
|
||||
fn from(task: BackgroundTask) -> Self {
|
||||
let (exit_code, summary, stdout_tail, stderr_tail) = match &task.result {
|
||||
Some(BackgroundTaskResult::Success {
|
||||
exit_code,
|
||||
summary,
|
||||
stdout_tail,
|
||||
stderr_tail,
|
||||
..
|
||||
}) => (
|
||||
*exit_code,
|
||||
Some(summary.clone()),
|
||||
stdout_tail.clone(),
|
||||
stderr_tail.clone(),
|
||||
),
|
||||
Some(BackgroundTaskResult::Failure {
|
||||
exit_code,
|
||||
error,
|
||||
stdout_tail,
|
||||
stderr_tail,
|
||||
..
|
||||
}) => (
|
||||
*exit_code,
|
||||
Some(error.clone()),
|
||||
stdout_tail.clone(),
|
||||
stderr_tail.clone(),
|
||||
),
|
||||
Some(BackgroundTaskResult::Cancelled { reason, .. })
|
||||
| Some(BackgroundTaskResult::Expired { reason, .. }) => {
|
||||
(None, Some(reason.clone()), None, None)
|
||||
}
|
||||
None => (None, None, None, None),
|
||||
};
|
||||
Self {
|
||||
task_id: task.id.to_string(),
|
||||
owner_agent_id: task.owner_agent_id.to_string(),
|
||||
project_id: task.project_id.to_string(),
|
||||
kind: background_kind_label(&task.kind).to_owned(),
|
||||
state: background_state_label(task.state).to_owned(),
|
||||
exit_code,
|
||||
summary,
|
||||
stdout_tail,
|
||||
stderr_tail,
|
||||
created_at_ms: task.created_at_ms,
|
||||
updated_at_ms: task.updated_at_ms,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a task-id string (UUID) coming from the frontend.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] with code `INVALID` if the string is not a UUID.
|
||||
pub fn parse_task_id(raw: &str) -> Result<TaskId, ErrorDto> {
|
||||
uuid::Uuid::parse_str(raw)
|
||||
.map(TaskId::from_uuid)
|
||||
.map_err(|_| ErrorDto {
|
||||
code: "INVALID".to_owned(),
|
||||
message: format!("invalid task id: {raw}"),
|
||||
})
|
||||
}
|
||||
|
||||
/// Request DTO for `spawn_background_command`.
|
||||
///
|
||||
/// Builds a command-backed [`BackgroundTask`]: the command line runs under the
|
||||
/// project host's PTY and its completion is delivered to `ownerAgentId`.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SpawnBackgroundCommandRequestDto {
|
||||
/// Owning project id (UUID string).
|
||||
pub project_id: String,
|
||||
/// Agent that owns completion delivery (UUID string).
|
||||
pub owner_agent_id: String,
|
||||
/// Human-facing label.
|
||||
pub label: String,
|
||||
/// Executable to run.
|
||||
pub command: String,
|
||||
/// Arguments.
|
||||
#[serde(default)]
|
||||
pub args: Vec<String>,
|
||||
/// Working directory (absolute path).
|
||||
pub cwd: String,
|
||||
/// Extra environment variables.
|
||||
#[serde(default)]
|
||||
pub env: Vec<(String, String)>,
|
||||
/// When `true` (the default), wake the owner on completion; otherwise only
|
||||
/// record it.
|
||||
#[serde(default)]
|
||||
pub record_only: bool,
|
||||
/// Optional absolute deadline, epoch milliseconds.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub deadline_ms: Option<u64>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user