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:
@ -30,7 +30,8 @@ use domain::ports::PtyHandle;
|
||||
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_template_id, parse_ticket_id, AgentDriftListDto, AgentDto, AgentListDto,
|
||||
parse_task_id, parse_template_id, parse_ticket_id, AgentDriftListDto, AgentDto, AgentListDto,
|
||||
BackgroundTaskDto,
|
||||
AssignSkillRequestDto, AttachLiveAgentRequestDto, AttachLiveAgentResponseDto,
|
||||
ChangeAgentProfileDto, ChangeAgentProfileRequestDto, ConfigureProfilesRequestDto,
|
||||
ConversationDetailsDto, CreateAgentFromTemplateRequestDto, CreateAgentRequestDto,
|
||||
@ -2555,3 +2556,160 @@ pub async fn resolve_memory_links(
|
||||
.map(MemoryLinksDto::from)
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Background tasks (B8 — first-class background command)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Maps a background-task port error to the frontend [`ErrorDto`] shape.
|
||||
fn background_error(err: domain::ports::BackgroundTaskPortError) -> ErrorDto {
|
||||
use domain::ports::BackgroundTaskPortError as E;
|
||||
let code = match &err {
|
||||
E::NotFound => "NOT_FOUND",
|
||||
E::AlreadyExists | E::Invalid(_) => "INVALID",
|
||||
E::Runner(_) => "PROCESS",
|
||||
E::Store(_) => "STORE",
|
||||
};
|
||||
ErrorDto {
|
||||
code: code.to_owned(),
|
||||
message: err.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `spawn_background_command` — start a command-backed first-class background
|
||||
/// task whose completion is delivered to the owning agent's inbox.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for a malformed id/path, `PROCESS` if the
|
||||
/// command cannot be spawned, `STORE` on persistence failure).
|
||||
#[tauri::command]
|
||||
pub async fn spawn_background_command(
|
||||
request: crate::dto::SpawnBackgroundCommandRequestDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<BackgroundTaskDto, ErrorDto> {
|
||||
let project_id = parse_project_id(&request.project_id)?;
|
||||
let owner_agent_id = parse_agent_id(&request.owner_agent_id)?;
|
||||
let cwd = domain::project::ProjectPath::new(request.cwd.clone()).map_err(|_| ErrorDto {
|
||||
code: "INVALID".to_owned(),
|
||||
message: format!("invalid working directory: {}", request.cwd),
|
||||
})?;
|
||||
let command = domain::ports::SpawnSpec {
|
||||
command: request.command,
|
||||
args: request.args,
|
||||
cwd,
|
||||
env: request.env,
|
||||
context_plan: None,
|
||||
sandbox: None,
|
||||
};
|
||||
let wake_policy = if request.record_only {
|
||||
domain::BackgroundTaskWakePolicy::RecordOnly
|
||||
} else {
|
||||
domain::BackgroundTaskWakePolicy::WakeOwner
|
||||
};
|
||||
state
|
||||
.spawn_background_command
|
||||
.execute(application::SpawnBackgroundCommandInput {
|
||||
project_id,
|
||||
owner_agent_id,
|
||||
label: request.label,
|
||||
command,
|
||||
wake_policy,
|
||||
deadline_ms: request.deadline_ms,
|
||||
})
|
||||
.await
|
||||
.map(|out| BackgroundTaskDto::from(out.task))
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `cancel_background_task` — request cancellation of a running background task.
|
||||
///
|
||||
/// The terminal `Cancelled` state is written by the completion sink; this returns
|
||||
/// the task as currently persisted (absent if it no longer exists).
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `PROCESS`/`STORE` on
|
||||
/// failure).
|
||||
#[tauri::command]
|
||||
pub async fn cancel_background_task(
|
||||
task_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Option<BackgroundTaskDto>, ErrorDto> {
|
||||
let id = parse_task_id(&task_id)?;
|
||||
state
|
||||
.cancel_background_task
|
||||
.execute(id)
|
||||
.await
|
||||
.map(|out| out.task.map(BackgroundTaskDto::from))
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `retry_background_task` — re-run a terminal command task under a **new** task
|
||||
/// id (the original id is never reused).
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for a malformed id or a non-retryable task,
|
||||
/// `NOT_FOUND` if the task is unknown, `PROCESS`/`STORE` on failure).
|
||||
#[tauri::command]
|
||||
pub async fn retry_background_task(
|
||||
task_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<BackgroundTaskDto, ErrorDto> {
|
||||
let id = parse_task_id(&task_id)?;
|
||||
state
|
||||
.retry_background_task
|
||||
.execute(id)
|
||||
.await
|
||||
.map(|out| BackgroundTaskDto::from(out.task))
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `list_background_tasks` — read the background-task read-model for a project,
|
||||
/// optionally narrowed to one owning agent.
|
||||
///
|
||||
/// Returns the union of the agent's open tasks and undelivered completions,
|
||||
/// filtered to `projectId`. Without `agentId`, only the project's undelivered
|
||||
/// completions are returned (the store cannot enumerate open tasks project-wide).
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `STORE` on failure).
|
||||
#[tauri::command]
|
||||
pub async fn list_background_tasks(
|
||||
project_id: String,
|
||||
agent_id: Option<String>,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<BackgroundTaskDto>, ErrorDto> {
|
||||
let project_id = parse_project_id(&project_id)?;
|
||||
let agent_id = match &agent_id {
|
||||
Some(raw) => Some(parse_agent_id(raw)?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let store = &state.background_task_store;
|
||||
let mut by_id: std::collections::HashMap<domain::TaskId, domain::BackgroundTask> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
if let Some(agent_id) = agent_id {
|
||||
for task in store
|
||||
.list_open_for_agent(agent_id)
|
||||
.await
|
||||
.map_err(background_error)?
|
||||
{
|
||||
by_id.insert(task.id, task);
|
||||
}
|
||||
}
|
||||
for task in store
|
||||
.list_undelivered_completions()
|
||||
.await
|
||||
.map_err(background_error)?
|
||||
{
|
||||
by_id.entry(task.id).or_insert(task);
|
||||
}
|
||||
|
||||
let mut tasks: Vec<domain::BackgroundTask> = by_id
|
||||
.into_values()
|
||||
.filter(|task| task.project_id == project_id)
|
||||
.filter(|task| agent_id.map_or(true, |a| task.owner_agent_id == a))
|
||||
.collect();
|
||||
tasks.sort_by_key(|task| (task.created_at_ms, task.id));
|
||||
Ok(tasks.into_iter().map(BackgroundTaskDto::from).collect())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user