feat(session-limits): LS1 — couche domaine (détection + plan de reprise)
Pose les briques pures du domaine pour la gestion des limites de session des agents (état en mémoire, aucun schéma de persistance modifié) : - session_limit.rs (nouveau) : SessionLimit, ResumePlan, RateLimitSource, plan_resume (calcul du plan de reprise annulable). - ports.rs : variante ReplyEvent::RateLimited. - readiness.rs : variante ReadinessSignal::RateLimited + classify. - profile.rs : RateLimitPattern + champ + builder. - events.rs : 5 variantes DomainEvent pour le cycle de vie limite/reprise. - lib.rs : module + re-exports. Tests QA inline (#[cfg(test)]) : 24 tests dédiés. `cargo test -p domain` = 165 passed / 0 failed, zéro régression. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -209,6 +209,55 @@ pub enum DomainEvent {
|
||||
/// Target profile's submit delay in ms. `None` ⇒ front default (~60 ms).
|
||||
submit_delay_ms: Option<u32>,
|
||||
},
|
||||
/// Un agent vient d'entrer en **limite de session/débit** (ARCHITECTURE §21).
|
||||
/// Publié quand le service de limite enregistre une nouvelle `SessionLimit` (niveau
|
||||
/// 1 structuré ou niveau 2 motif). Balise discrète, basse fréquence, relayée au
|
||||
/// front pour afficher le badge « limité jusqu'à HH:MM ». Model-agnostique : ne
|
||||
/// porte que le fait neutre « limité, reset à T (peut-être) ».
|
||||
AgentRateLimited {
|
||||
/// L'agent entré en limite.
|
||||
agent_id: AgentId,
|
||||
/// Instant de reset en **époche-millisecondes**. `None` ⇒ heure inconnue
|
||||
/// (pas de reprise auto, filet humain).
|
||||
resets_at_ms: Option<i64>,
|
||||
},
|
||||
/// Une **reprise automatique** a été armée pour un agent limité (ARCHITECTURE §21).
|
||||
/// Publié après que le service a calculé le plan ([`crate::session_limit::plan_resume`])
|
||||
/// et armé le `Scheduler`. Relayé au front pour afficher le compte à rebours + le
|
||||
/// bouton « Annuler la reprise » (fenêtre annulable).
|
||||
AgentResumeScheduled {
|
||||
/// L'agent dont la reprise est programmée.
|
||||
agent_id: AgentId,
|
||||
/// Échéance du réveil en **époche-millisecondes**.
|
||||
fire_at_ms: i64,
|
||||
},
|
||||
/// La **reprise automatique** d'un agent a été **annulée** (ARCHITECTURE §21) :
|
||||
/// l'utilisateur a cliqué « Annuler la reprise » dans la fenêtre annulable. Relayé
|
||||
/// au front pour retirer le compte à rebours.
|
||||
AgentResumeCancelled {
|
||||
/// L'agent dont la reprise a été annulée.
|
||||
agent_id: AgentId,
|
||||
},
|
||||
/// Un agent a effectivement été **relancé** après une limite (ARCHITECTURE §21) :
|
||||
/// le réveil a tiré (ou reprise immédiate), l'agent a redémarré via
|
||||
/// [`crate::ports::SessionPlan::Resume`] avec un prompt de reprise court. Relayé au
|
||||
/// front pour effacer l'état « limité ».
|
||||
AgentResumed {
|
||||
/// L'agent relancé.
|
||||
agent_id: AgentId,
|
||||
},
|
||||
/// **Filet humain (niveau 3)** : une limite de session est **suspectée** sans
|
||||
/// qu'aucune heure de reset fiable ne soit connue (ARCHITECTURE §21.1 niveau 3) —
|
||||
/// typiquement un agent passé `Stalled` (lot 2) sans `SessionLimit` connue. IdeA ne
|
||||
/// reprend **jamais** à l'aveugle : ce signal demande au front de **solliciter
|
||||
/// l'utilisateur** (« limite détectée mais heure inconnue — reprendre à ? »).
|
||||
AgentRateLimitSuspected {
|
||||
/// L'agent dont la limite est suspectée.
|
||||
agent_id: AgentId,
|
||||
/// Instant de reset en **époche-millisecondes** si une estimation existe,
|
||||
/// sinon `None` (l'utilisateur fournira l'heure).
|
||||
resets_at_ms: Option<i64>,
|
||||
},
|
||||
/// Raw PTY output (usually routed to a dedicated channel, not this bus).
|
||||
PtyOutput {
|
||||
/// The session.
|
||||
@ -217,3 +266,104 @@ pub enum DomainEvent {
|
||||
bytes: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn agent(n: u128) -> AgentId {
|
||||
AgentId::from_uuid(uuid::Uuid::from_u128(n))
|
||||
}
|
||||
|
||||
// -- §21 : constructibilité + égalité PartialEq des 5 variantes --------------
|
||||
|
||||
#[test]
|
||||
fn agent_rate_limited_constructs_and_compares() {
|
||||
let ev = DomainEvent::AgentRateLimited {
|
||||
agent_id: agent(1),
|
||||
resets_at_ms: Some(1_700_000_000_000),
|
||||
};
|
||||
assert_eq!(
|
||||
ev,
|
||||
DomainEvent::AgentRateLimited {
|
||||
agent_id: agent(1),
|
||||
resets_at_ms: Some(1_700_000_000_000),
|
||||
}
|
||||
);
|
||||
// Une heure différente ⇒ inégaux.
|
||||
assert_ne!(
|
||||
ev,
|
||||
DomainEvent::AgentRateLimited {
|
||||
agent_id: agent(1),
|
||||
resets_at_ms: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_resume_scheduled_constructs_and_compares() {
|
||||
let ev = DomainEvent::AgentResumeScheduled {
|
||||
agent_id: agent(2),
|
||||
fire_at_ms: 1_700_000_000_000,
|
||||
};
|
||||
assert_eq!(
|
||||
ev,
|
||||
DomainEvent::AgentResumeScheduled {
|
||||
agent_id: agent(2),
|
||||
fire_at_ms: 1_700_000_000_000,
|
||||
}
|
||||
);
|
||||
assert_ne!(
|
||||
ev,
|
||||
DomainEvent::AgentResumeScheduled {
|
||||
agent_id: agent(2),
|
||||
fire_at_ms: 0,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_resume_cancelled_constructs_and_compares() {
|
||||
let ev = DomainEvent::AgentResumeCancelled { agent_id: agent(3) };
|
||||
assert_eq!(ev, DomainEvent::AgentResumeCancelled { agent_id: agent(3) });
|
||||
assert_ne!(ev, DomainEvent::AgentResumeCancelled { agent_id: agent(4) });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_resumed_constructs_and_compares() {
|
||||
let ev = DomainEvent::AgentResumed { agent_id: agent(5) };
|
||||
assert_eq!(ev, DomainEvent::AgentResumed { agent_id: agent(5) });
|
||||
assert_ne!(ev, DomainEvent::AgentResumed { agent_id: agent(6) });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_rate_limit_suspected_constructs_and_compares() {
|
||||
let ev = DomainEvent::AgentRateLimitSuspected {
|
||||
agent_id: agent(7),
|
||||
resets_at_ms: None,
|
||||
};
|
||||
assert_eq!(
|
||||
ev,
|
||||
DomainEvent::AgentRateLimitSuspected {
|
||||
agent_id: agent(7),
|
||||
resets_at_ms: None,
|
||||
}
|
||||
);
|
||||
assert_ne!(
|
||||
ev,
|
||||
DomainEvent::AgentRateLimitSuspected {
|
||||
agent_id: agent(7),
|
||||
resets_at_ms: Some(42),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distinct_session_limit_variants_are_not_equal() {
|
||||
// Les variantes ne se confondent pas entre elles malgré des champs proches.
|
||||
assert_ne!(
|
||||
DomainEvent::AgentResumeCancelled { agent_id: agent(8) },
|
||||
DomainEvent::AgentResumed { agent_id: agent(8) }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user