feat(backend): modèle unifié streaming/progress/events provider-agnostic + pont app-tauri — foundation #156 (QA verte)
This commit is contained in:
@ -596,6 +596,121 @@ pub enum WakeReason {
|
||||
},
|
||||
}
|
||||
|
||||
/// Origine canonique d'un événement de progression intermédiaire.
|
||||
///
|
||||
/// La séparation est volontairement métier-neutre : un adapter peut relayer un fait
|
||||
/// natif du provider, tandis qu'IdeA peut produire sa propre observabilité locale
|
||||
/// (MCP/outils orchestrés) sans prétendre que le provider l'a streamée.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ReplyProgressSource {
|
||||
/// Événement observé dans le flux/documentation du provider.
|
||||
ProviderNative,
|
||||
/// Événement produit par IdeA autour d'un outil, d'un appel MCP ou d'une
|
||||
/// orchestration locale.
|
||||
IdeaLocal,
|
||||
}
|
||||
|
||||
/// Taxonomie stable des progress/events non terminaux projetables avant `Final`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ReplyProgressKind {
|
||||
/// Début/fin/étape de tour sans contenu assistant exploitable.
|
||||
Turn,
|
||||
/// Message/progression textuelle intermédiaire.
|
||||
Message,
|
||||
/// Appel ou activité d'outil du provider.
|
||||
Tool,
|
||||
/// Appel MCP ou outil orchestré par IdeA.
|
||||
Mcp,
|
||||
/// Événement conservé sans spécialisation plus fine.
|
||||
Other,
|
||||
}
|
||||
|
||||
/// Étape canonique d'un événement intermédiaire.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ReplyProgressStage {
|
||||
/// Démarrage d'une activité.
|
||||
Started,
|
||||
/// Fragment ou mise à jour en cours.
|
||||
Delta,
|
||||
/// Fin d'une activité.
|
||||
Completed,
|
||||
/// Information ponctuelle.
|
||||
Info,
|
||||
}
|
||||
|
||||
/// Événement de progression canonique, provider-agnostic et best-effort.
|
||||
///
|
||||
/// Aucun champ n'a autorité sur la fin de tour : seul [`ReplyEvent::Final`] reste
|
||||
/// terminal. `provider` / `native_event` sont des métadonnées d'observabilité, pas
|
||||
/// un contrat métier à parser côté application.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ReplyProgress {
|
||||
/// Origine de l'événement (provider natif ou observabilité locale IdeA).
|
||||
pub source: ReplyProgressSource,
|
||||
/// Famille canonique.
|
||||
pub kind: ReplyProgressKind,
|
||||
/// Étape canonique.
|
||||
pub stage: ReplyProgressStage,
|
||||
/// Libellé court affichable.
|
||||
pub label: String,
|
||||
/// Texte/extrait optionnel affichable, déjà nettoyé par l'adapter.
|
||||
pub text: Option<String>,
|
||||
/// Identité provider optionnelle (`codex`, `claude`, `openai-compatible`, ...).
|
||||
pub provider: Option<String>,
|
||||
/// Nom du type natif observé, quand disponible (`turn.started`, `tool_use`, ...).
|
||||
pub native_event: Option<String>,
|
||||
/// Nom de l'outil/MCP, quand applicable.
|
||||
pub tool_name: Option<String>,
|
||||
}
|
||||
|
||||
impl ReplyProgress {
|
||||
/// Construit un événement de progression avec les champs obligatoires.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
source: ReplyProgressSource,
|
||||
kind: ReplyProgressKind,
|
||||
stage: ReplyProgressStage,
|
||||
label: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
source,
|
||||
kind,
|
||||
stage,
|
||||
label: label.into(),
|
||||
text: None,
|
||||
provider: None,
|
||||
native_event: None,
|
||||
tool_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Ajoute un extrait textuel.
|
||||
#[must_use]
|
||||
pub fn with_text(mut self, text: impl Into<String>) -> Self {
|
||||
self.text = Some(text.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Ajoute les métadonnées provider natives.
|
||||
#[must_use]
|
||||
pub fn with_provider_event(
|
||||
mut self,
|
||||
provider: impl Into<String>,
|
||||
native_event: impl Into<String>,
|
||||
) -> Self {
|
||||
self.provider = Some(provider.into());
|
||||
self.native_event = Some(native_event.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Ajoute le nom d'outil/MCP associé.
|
||||
#[must_use]
|
||||
pub fn with_tool_name(mut self, tool_name: impl Into<String>) -> Self {
|
||||
self.tool_name = Some(tool_name.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// 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é
|
||||
@ -604,6 +719,13 @@ pub enum WakeReason {
|
||||
/// frontière domaine.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ReplyEvent {
|
||||
/// Progress/event intermédiaire canonique et non terminal, utilisable pour
|
||||
/// relayer les événements provider natifs et l'observabilité locale IdeA avant
|
||||
/// le `Final`.
|
||||
Progress {
|
||||
/// Événement de progression normalisé.
|
||||
progress: ReplyProgress,
|
||||
},
|
||||
/// Un fragment de texte assistant (rendu incrémental côté UI chat).
|
||||
TextDelta {
|
||||
/// Le fragment de texte.
|
||||
|
||||
@ -84,7 +84,8 @@ impl ReadinessPolicy {
|
||||
/// n'a pas rendu son `Final`), mais porteur d'un signal exploitable par
|
||||
/// l'application (planifier la reprise à `resets_at_ms`). L'heure de reset est
|
||||
/// propagée telle quelle.
|
||||
/// - [`ReplyEvent::TextDelta`] / [`ReplyEvent::Announcement`] /
|
||||
/// - [`ReplyEvent::Progress`] /
|
||||
/// [`ReplyEvent::TextDelta`] / [`ReplyEvent::Announcement`] /
|
||||
/// [`ReplyEvent::Error`] /
|
||||
/// [`ReplyEvent::ToolActivity`] /
|
||||
/// [`ReplyEvent::Heartbeat`] ⇒ `None` : tous **non terminaux** (le flux
|
||||
@ -96,7 +97,8 @@ impl ReadinessPolicy {
|
||||
ReplyEvent::RateLimited { resets_at_ms } => Some(ReadinessSignal::RateLimited {
|
||||
resets_at_ms: *resets_at_ms,
|
||||
}),
|
||||
ReplyEvent::TextDelta { .. }
|
||||
ReplyEvent::Progress { .. }
|
||||
| ReplyEvent::TextDelta { .. }
|
||||
| ReplyEvent::Announcement { .. }
|
||||
| ReplyEvent::Error { .. }
|
||||
| ReplyEvent::ToolActivity { .. }
|
||||
@ -108,6 +110,7 @@ impl ReadinessPolicy {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ports::{ReplyProgress, ReplyProgressKind, ReplyProgressSource, ReplyProgressStage};
|
||||
|
||||
#[test]
|
||||
fn final_classifies_as_turn_ended() {
|
||||
@ -122,6 +125,18 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn deltas_activities_and_heartbeats_are_non_terminal() {
|
||||
assert_eq!(
|
||||
ReadinessPolicy::classify(&ReplyEvent::Progress {
|
||||
progress: ReplyProgress::new(
|
||||
ReplyProgressSource::ProviderNative,
|
||||
ReplyProgressKind::Turn,
|
||||
ReplyProgressStage::Started,
|
||||
"tour démarré",
|
||||
)
|
||||
}),
|
||||
None,
|
||||
"un progress provider est projetable avant Final mais ne termine JAMAIS le tour"
|
||||
);
|
||||
assert_eq!(
|
||||
ReadinessPolicy::classify(&ReplyEvent::TextDelta { text: "x".into() }),
|
||||
None
|
||||
|
||||
Reference in New Issue
Block a user