fix(ticket-assistant): rendre visible l'échec « aucune réponse » de l'assistant de ticket (#60)

Introduit un variant terminal `ReplyEvent::Error { message }` (domain/ports)
propagé jusqu'au chat : l'adapter OpenAI-compat récupère `reasoning_content`
quand `content` est vide, et un `Final` vide est converti en `Error` visible
plutôt qu'un tour silencieux qui pend. Le front (`ReplyChunk` + useTicketAssistant)
rend ce message d'erreur dans la cellule.

- domain: variant `ReplyEvent::Error`, bras non terminal (readiness, structured drain)
- infra/session: parse `reasoning_content` (delta/completion), fallback visible
- app-tauri: mapping ReplyEvent::Error -> ReplyChunk::Error, Final vide -> Error
- frontend: ReplyChunk `error`, rendu dans useTicketAssistant (+ test .test.tsx)

NB: commit sur la feature branch, PAS un merge. #60 reste inProgress.
Les 10 tests `cargo test -p infrastructure --lib session` échouent SOUS SANDBOX
uniquement (bind loopback 127.0.0.1:0 interdit -> Os PermissionDenied), pas une
régression : revérification hors-sandbox requise avant tout merge vers develop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 08:32:59 +02:00
parent ce5aa2811c
commit 2f7f111545
14 changed files with 571 additions and 25 deletions

View File

@ -405,6 +405,13 @@ pub enum ReplyEvent {
/// humain, §21.1 niveau 3). Jamais une `Instant` monotone (cf. §21.2-T1).
resets_at_ms: Option<i64>,
},
/// Erreur terminale du moteur structurée et affichable côté chat. Porte un
/// message déjà nettoyé par l'adapter ; les détails propres à la CLI restent
/// confinés côté infrastructure.
Error {
/// Message humain-lisible à rendre dans la cellule chat.
message: String,
},
/// **Événement terminal déterministe** d'un tour : l'adapter l'émet quand il a
/// lu le message `result` documenté de la CLI. Porte le contenu final agrégé.
/// Après `Final`, le flux se termine (plus aucun événement).
@ -415,11 +422,13 @@ pub enum ReplyEvent {
}
/// Flux borné d'événements de réponse d'UN tour (ARCHITECTURE §17.1). Se termine
/// après le [`ReplyEvent::Final`] (ou sur erreur). Calqué sur [`OutputStream`],
/// après le [`ReplyEvent::Final`] / [`ReplyEvent::Error`] (ou sur erreur de transport).
/// Calqué sur [`OutputStream`],
/// mais **typé** : deltas de texte → activités d'outil → battements de cœur*
/// ([`ReplyEvent::Heartbeat`], non terminaux, en nombre quelconque et entrelacés) →
/// **un** `Final` terminal déterministe, plutôt que des octets bruts. Seul le `Final`
/// clôt le flux ; deltas, activités et heartbeats sont tous non terminaux.
/// **un** terminal déterministe, plutôt que des octets bruts. `Final` clôt un tour
/// réussi ; `Error` clôt un tour échoué avec un message visible. Deltas, activités
/// et heartbeats sont tous non terminaux.
pub type ReplyStream = Box<dyn Iterator<Item = ReplyEvent> + Send>;
/// Description pure d'un outil exposé à un modèle OpenAI-compatible.

View File

@ -85,6 +85,7 @@ impl ReadinessPolicy {
/// l'application (planifier la reprise à `resets_at_ms`). L'heure de reset est
/// propagée telle quelle.
/// - [`ReplyEvent::TextDelta`] / [`ReplyEvent::Announcement`] /
/// [`ReplyEvent::Error`] /
/// [`ReplyEvent::ToolActivity`] /
/// [`ReplyEvent::Heartbeat`] ⇒ `None` : tous **non terminaux** (le flux
/// continue). Un heartbeat prouve la vivacité mais ne termine pas le tour.
@ -97,6 +98,7 @@ impl ReadinessPolicy {
}),
ReplyEvent::TextDelta { .. }
| ReplyEvent::Announcement { .. }
| ReplyEvent::Error { .. }
| ReplyEvent::ToolActivity { .. }
| ReplyEvent::Heartbeat => None,
}