feat(domain): modèle de tâches de fond de 1re classe et mailbox (B1)

Entités BackgroundTask et Inbox, identifiants, événements et ports du
domaine pour la récupération de complétion post-tour et la réception de
messages concurrents. Fondations des lots infra/application suivants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 15:47:16 +02:00
parent fccc1e2f0f
commit f4a55e9988
7 changed files with 1276 additions and 11 deletions

View File

@ -28,8 +28,11 @@ use async_trait::async_trait;
use thiserror::Error;
use crate::agent::AgentManifest;
use crate::background_task::{
BackgroundTask, BackgroundTaskKind, BackgroundTaskResult, BackgroundTaskWakePolicy,
};
use crate::events::DomainEvent;
use crate::ids::{AgentId, NodeId, ScheduleId, SessionId};
use crate::ids::{AgentId, NodeId, ProjectId, ScheduleId, SessionId, TaskId};
use crate::markdown::MarkdownDoc;
use crate::memory::{Memory, MemoryIndexEntry, MemoryLink, MemorySlug};
use crate::permission::ProjectPermissions;
@ -196,6 +199,59 @@ pub type OutputStream = Box<dyn Iterator<Item = Vec<u8>> + Send>;
/// A boxed stream of domain events, returned by [`EventBus::subscribe`].
pub type EventStream = Box<dyn Iterator<Item = DomainEvent> + Send>;
/// A boxed stream of background task completions.
pub type BackgroundCompletionStream = Box<dyn Iterator<Item = BackgroundTaskCompletion> + Send>;
/// Specification passed to a background task runner.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackgroundTaskSpec {
/// Task id allocated by the application.
pub task_id: TaskId,
/// Owning project.
pub project_id: ProjectId,
/// Agent that owns completion delivery.
pub owner_agent_id: AgentId,
/// Kind-specific task payload.
pub kind: BackgroundTaskKind,
/// Completion wake policy.
pub wake_policy: BackgroundTaskWakePolicy,
/// Optional command invocation for command-backed tasks.
pub command: Option<SpawnSpec>,
/// Optional absolute deadline, epoch milliseconds.
pub deadline_ms: Option<u64>,
}
/// Opaque runner handle for a spawned background task.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackgroundTaskHandle {
/// Spawned task id.
pub task_id: TaskId,
}
/// Terminal completion reported by a background runner.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackgroundTaskCompletion {
/// Completed task id.
pub task_id: TaskId,
/// Terminal result.
pub result: BackgroundTaskResult,
}
/// Why IdeA wakes an agent outside a human/delegation submit.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WakeReason {
/// A persisted background task completion is ready in the owner's inbox.
BackgroundCompletion {
/// Completed background task.
task_id: TaskId,
},
/// A generic queued inbox item should be processed.
InboxItem {
/// Queued item id.
item_id: crate::mailbox::TicketId,
},
}
/// Un événement incrémental d'un tour de réponse d'un agent IA (ARCHITECTURE §17.1).
///
/// Universel : l'adapter (Claude/Codex) traduit SON format structuré documenté
@ -461,6 +517,55 @@ pub enum GitError {
Operation(String),
}
/// Errors from first-class background task ports.
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum BackgroundTaskPortError {
/// The requested task was not found.
#[error("background task not found")]
NotFound,
/// A task with this id already exists.
#[error("background task already exists")]
AlreadyExists,
/// The task or request is invalid.
#[error("background task invalid: {0}")]
Invalid(String),
/// The runner failed.
#[error("background task runner failed: {0}")]
Runner(String),
/// The store failed.
#[error("background task store failed: {0}")]
Store(String),
}
/// Errors from the owner wake port.
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum WakeError {
/// The owner is already running a turn; the inbox item must stay queued.
#[error("agent {agent_id} is busy; wake skipped")]
AgentBusy {
/// Target agent.
agent_id: AgentId,
},
/// There is no queued item to drain for this wake.
#[error("no pending inbox item for agent {agent_id}")]
NoPendingItem {
/// Target agent.
agent_id: AgentId,
},
/// The drained inbox item does not match the requested wake reason.
#[error("unexpected inbox item for wake: {0}")]
UnexpectedInboxItem(String),
/// Session lookup or launch failed.
#[error("wake session failed: {0}")]
Session(String),
/// Background task store failed.
#[error("wake store failed: {0}")]
Store(String),
/// Background task payload was missing or invalid for wake delivery.
#[error("wake task invalid: {0}")]
Task(String),
}
// ---------------------------------------------------------------------------
// Git port support types
// ---------------------------------------------------------------------------
@ -1191,6 +1296,95 @@ pub trait PermissionStore: Send + Sync {
) -> Result<(), StoreError>;
}
/// Persistence port for first-class background tasks.
#[async_trait]
pub trait BackgroundTaskStore: Send + Sync {
/// Creates a new task.
///
/// # Errors
/// [`BackgroundTaskPortError`] if the task already exists or cannot be stored.
async fn create(&self, task: &BackgroundTask) -> Result<(), BackgroundTaskPortError>;
/// Gets a task by id.
///
/// # Errors
/// [`BackgroundTaskPortError`] on store failure.
async fn get(&self, id: TaskId) -> Result<Option<BackgroundTask>, BackgroundTaskPortError>;
/// Saves a task.
///
/// # Errors
/// [`BackgroundTaskPortError`] on store failure.
async fn save(&self, task: &BackgroundTask) -> Result<(), BackgroundTaskPortError>;
/// Lists non-terminal tasks owned by an agent.
///
/// # Errors
/// [`BackgroundTaskPortError`] on store failure.
async fn list_open_for_agent(
&self,
agent_id: AgentId,
) -> Result<Vec<BackgroundTask>, BackgroundTaskPortError>;
/// Lists terminal tasks whose completion has not been delivered yet.
///
/// # Errors
/// [`BackgroundTaskPortError`] on store failure.
async fn list_undelivered_completions(
&self,
) -> Result<Vec<BackgroundTask>, BackgroundTaskPortError>;
/// Marks a completion as delivered.
///
/// # Errors
/// [`BackgroundTaskPortError`] on store failure.
async fn mark_completion_delivered(
&self,
task_id: TaskId,
) -> Result<(), BackgroundTaskPortError>;
}
/// Execution port for first-class background tasks.
#[async_trait]
pub trait BackgroundTaskRunner: Send + Sync {
/// Spawns a background task.
///
/// # Errors
/// [`BackgroundTaskPortError`] when the task cannot be spawned.
async fn spawn(
&self,
spec: BackgroundTaskSpec,
) -> Result<BackgroundTaskHandle, BackgroundTaskPortError>;
/// Cancels a running task.
///
/// # Errors
/// [`BackgroundTaskPortError`] when cancellation fails.
async fn cancel(&self, task_id: TaskId) -> Result<(), BackgroundTaskPortError>;
/// Subscribes to terminal completions.
fn subscribe_completions(&self) -> BackgroundCompletionStream;
}
/// Wakes an agent to process one queued system/inbox item.
#[async_trait]
pub trait AgentWakePort: Send + Sync {
/// Wakes `agent` in `project` for `reason`, if the agent is idle.
///
/// Implementations must drain at most one inbox item and must not start a
/// concurrent turn when the agent is already busy.
///
/// # Errors
/// [`WakeError`] when the session/store operation fails or the queued item is
/// inconsistent with the requested wake.
async fn wake_agent(
&self,
project: &Project,
agent: AgentId,
reason: WakeReason,
) -> Result<(), WakeError>;
}
/// Git operations for a project. Named `GitPort` to avoid clashing with the
/// [`crate::git::GitRepository`] *entity* (state image).
#[async_trait]