feat(inter-agent): backend des annonces live d'agent (B0-B3)

Redémarre le ticket #4 sur la base develop. Émission d'annonces live d'un
agent vers l'UI, distinctes du Final de délégation :

- domain: ReplyEvent::Announcement/Final et DomainEvent::AgentAnnouncement
  (events.rs), port d'émission (ports.rs), gating de readiness (readiness.rs).
- application: mapping des événements structurés en annonces
  (agent/structured.rs, agent/mod.rs, lib.rs) et relais côté orchestrateur
  (orchestrator/service.rs).
- infrastructure/session: parse des annonces + fix du Final pour Claude et
  Codex, propagé aux adaptateurs et à la conformance
  (claude.rs, codex.rs, conformance.rs, mod.rs, process.rs, sandbox_e2e.rs).
- app-tauri: relais Tauri des annonces vers le front (events.rs, chat.rs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 19:50:03 +02:00
parent ebd992e41a
commit aa5a4f30ae
15 changed files with 605 additions and 52 deletions

View File

@ -1,6 +1,7 @@
//! Domain events published on the [`crate::ports::EventBus`] and relayed to the
//! presentation layer (ARCHITECTURE §3.2).
use crate::conversation::ConversationParty;
use crate::ids::{AgentId, IssueId, ProfileId, ProjectId, SessionId, SkillId, TaskId, TemplateId};
use crate::issue::{IssueLinkKind, IssuePriority, IssueRef, IssueStatus, IssueVersion};
use crate::mailbox::TicketId;
@ -156,6 +157,22 @@ pub enum DomainEvent {
/// Number of bytes in the reply content (preview metric, not the payload).
reply_len: usize,
},
/// Intermediate assistant announcement emitted live during an inter-agent
/// structured turn. Ephemeral observability event: never persisted, never terminal.
AgentAnnouncement {
/// Owning project.
project_id: ProjectId,
/// Human or source agent that requested the turn.
requester: ConversationParty,
/// Target agent producing the announcement.
target: AgentId,
/// FIFO ticket correlating this turn.
ticket: TicketId,
/// Announcement text.
text: String,
/// Wall-clock timestamp in epoch milliseconds.
at_ms: u64,
},
/// An agent's process exited.
AgentExited {
/// The agent.

View File

@ -275,6 +275,12 @@ pub enum ReplyEvent {
/// Libellé humain-lisible de l'activité.
label: String,
},
/// Message assistant intermédiaire complet, destiné à l'observabilité live d'un
/// rendez-vous inter-agent. Non terminal : seul [`ReplyEvent::Final`] clôt le tour.
Announcement {
/// Texte complet de l'annonce.
text: String,
},
/// **Preuve de vivacité non terminale** (model-agnostique) : l'agent est
/// toujours en train de travailler. Émis par l'adapter quand le moteur signale
/// un battement de cœur natif **sans contenu utile** (handshake/init, fenêtre de
@ -688,6 +694,20 @@ pub trait AgentSession: Send + Sync {
/// communication/décodage.
async fn send(&self, prompt: &str) -> Result<ReplyStream, AgentSessionError>;
/// Comme [`AgentSession::send`], avec un tap optionnel d'événements live produits
/// pendant le tour avant que le flux batch final soit rendu. L'implémentation par
/// défaut préserve les sessions/fakes existants : aucun tap, puis [`send`](Self::send).
///
/// # Errors
/// Identiques à [`AgentSession::send`].
async fn send_with_tap(
&self,
prompt: &str,
_tap: std::sync::mpsc::Sender<ReplyEvent>,
) -> Result<ReplyStream, AgentSessionError> {
self.send(prompt).await
}
/// Termine proprement la session (tue le process/SDK sous-jacent). Idempotent.
///
/// # Errors

View File

@ -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::ToolActivity`] /
/// - [`ReplyEvent::TextDelta`] / [`ReplyEvent::Announcement`] /
/// [`ReplyEvent::ToolActivity`] /
/// [`ReplyEvent::Heartbeat`] ⇒ `None` : tous **non terminaux** (le flux
/// continue). Un heartbeat prouve la vivacité mais ne termine pas le tour.
#[must_use]
@ -95,6 +96,7 @@ impl ReadinessPolicy {
resets_at_ms: *resets_at_ms,
}),
ReplyEvent::TextDelta { .. }
| ReplyEvent::Announcement { .. }
| ReplyEvent::ToolActivity { .. }
| ReplyEvent::Heartbeat => None,
}