feat(session-limits): LS3 — port Scheduler + adapter TokioScheduler
Introduit l'abstraction de planification pour la reprise différée à la levée d'une limite de session : - domaine : trait Scheduler + enum ScheduledTask (ports.rs), ScheduleId via typed_id! (ids.rs), re-exports (lib.rs). - infra : TokioScheduler (scheduler/mod.rs, nouveau) + pub mod scheduler et re-export (lib.rs). Tests QA inline (#[cfg(test)]) : 7 tests scheduler (3× sans flaky). `cargo test -p infrastructure` = 195 passed / 0 failed ; domaine + infra builds 0 warning ; LS1/LS2 toujours verts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -29,7 +29,7 @@ use thiserror::Error;
|
||||
|
||||
use crate::agent::AgentManifest;
|
||||
use crate::events::DomainEvent;
|
||||
use crate::ids::{AgentId, SessionId};
|
||||
use crate::ids::{AgentId, NodeId, ScheduleId, SessionId};
|
||||
use crate::markdown::MarkdownDoc;
|
||||
use crate::memory::{Memory, MemoryIndexEntry, MemoryLink, MemorySlug};
|
||||
use crate::permission::ProjectPermissions;
|
||||
@ -262,6 +262,34 @@ pub enum ReplyEvent {
|
||||
/// clôt le flux ; deltas, activités et heartbeats sont tous non terminaux.
|
||||
pub type ReplyStream = Box<dyn Iterator<Item = ReplyEvent> + Send>;
|
||||
|
||||
/// Intention **model-agnostique** exécutée à l'échéance d'un réveil de
|
||||
/// [`Scheduler`] (ARCHITECTURE §21.4).
|
||||
///
|
||||
/// **Donnée pure, pas de closure** : à l'image du dispatch de l'orchestrateur
|
||||
/// (§14.3, où une requête est une *donnée* validée puis dispatchée), une tâche
|
||||
/// programmée est une **valeur** qui franchit la frontière, jamais un effet ni une
|
||||
/// fonction. C'est ce qui garde le port [`Scheduler`] sans aucune dépendance vers
|
||||
/// l'application : l'adapter ne fait que **remettre** cette donnée à un drain
|
||||
/// applicatif, qui seul l'exécute.
|
||||
///
|
||||
/// Enum **extensible** : d'autres intentions programmées pourront s'ajouter sans
|
||||
/// toucher au port (Open/Closed).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ScheduledTask {
|
||||
/// Reprendre un agent à l'heure de reset de sa limite de session (§21) : relancer
|
||||
/// via [`SessionPlan::Resume`] avec un prompt de reprise court (logique côté
|
||||
/// application, lot LS4). Porte exactement le pivot de reprise model-agnostique.
|
||||
ResumeAgent {
|
||||
/// L'agent à reprendre.
|
||||
agent_id: AgentId,
|
||||
/// La cellule (nœud du layout) qui héberge sa session.
|
||||
node_id: NodeId,
|
||||
/// Id de conversation du moteur à reprendre (`None` ⇒ reprise dégradée sans
|
||||
/// id, comme le reste du chemin de reprise §15).
|
||||
conversation_id: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Per-port error types
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -1266,3 +1294,50 @@ pub trait IdGenerator: Send + Sync {
|
||||
/// Returns a fresh UUID.
|
||||
fn new_uuid(&self) -> uuid::Uuid;
|
||||
}
|
||||
|
||||
/// Minuterie **one-shot annulable** (ARCHITECTURE §21.4) — le **seul** port neuf de
|
||||
/// la feature « limites de session ». Là où [`Clock`] dit *quelle heure il est*, ce
|
||||
/// port *réveille* à une échéance absolue donnée.
|
||||
///
|
||||
/// # Interface Segregation (§21.8-I)
|
||||
/// Volontairement minimal — `arm` / `cancel`, rien d'autre — et **distinct** de
|
||||
/// [`Clock`] : un consommateur qui n'a besoin que de « réveille-moi à T » ne dépend
|
||||
/// pas d'un store ni d'une horloge.
|
||||
///
|
||||
/// # Synchrone (pas d'`async_trait`)
|
||||
/// Comme [`Clock`], [`IdGenerator`] et [`EventBus`], ce port est **non bloquant** :
|
||||
/// `arm` ne fait qu'enregistrer une minuterie (l'attente réelle se passe en tâche de
|
||||
/// fond dans l'adapter) et `cancel` ne fait qu'annuler — aucun `await` au point
|
||||
/// d'appel. On le garde donc en `fn` simple, sans payer le boxing d'`async_trait`.
|
||||
///
|
||||
/// # Remise de la tâche échue — dispatch par DONNÉE (§14.3, §21.4)
|
||||
/// Le port **n'exécute jamais** la reprise (aucune dépendance vers l'application).
|
||||
/// À l'échéance, l'adapter **pousse la [`ScheduledTask`] (une valeur)** dans un canal
|
||||
/// fourni à sa construction, que l'application **draine** et exécute — exactement le
|
||||
/// patron du watcher d'orchestrateur, qui dispatche une requête-donnée vers
|
||||
/// l'`OrchestratorService`. Aucune closure ne franchit la frontière domaine.
|
||||
///
|
||||
/// # État en mémoire (§21.1-3)
|
||||
/// Aucune persistance : un réveil armé ne survit pas à un redémarrage d'IdeA (le
|
||||
/// chemin `ListResumableAgents` existant prend alors le relais).
|
||||
///
|
||||
/// # Substituabilité (Liskov)
|
||||
/// Tout adapter (le `TokioScheduler` réel comme un fake de test qui tire à la
|
||||
/// demande) respecte le même contrat : `arm` rend un id annulable ; `cancel` renvoie
|
||||
/// `true` ssi il a effectivement désarmé un réveil non encore tiré.
|
||||
pub trait Scheduler: Send + Sync {
|
||||
/// Arme une minuterie one-shot à `deadline_ms` (**époche-millisecondes absolues**,
|
||||
/// cohérent avec [`Clock::now_millis`] et `ResumePlan::Scheduled.fire_at_ms`). À
|
||||
/// l'échéance, l'adapter remet `task` au drain applicatif (cf. doc du trait).
|
||||
/// Renvoie un [`ScheduleId`] annulable.
|
||||
///
|
||||
/// `deadline_ms` **≤ now** ⇒ déclenchement **au plus tôt** (immédiat). Le clamp
|
||||
/// anti-passé est déjà assuré en amont par `plan_resume` (domaine) ; ce contrat ne
|
||||
/// fait que garantir qu'une échéance passée ne « se perd » pas.
|
||||
fn arm(&self, deadline_ms: i64, task: ScheduledTask) -> ScheduleId;
|
||||
|
||||
/// Annule un réveil armé **non encore tiré**. Idempotent et **sans erreur** :
|
||||
/// renvoie `true` s'il a effectivement été désarmé, `false` s'il était inconnu ou
|
||||
/// déjà tiré. C'est ce qui sous-tend la « reprise auto **annulable** » (§21.1-4).
|
||||
fn cancel(&self, id: ScheduleId) -> bool;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user