fix(orchestrator,agent): rendezvous cross-project réarmé par jeton d'activité live model-agnostique — #287 (le watchdog d'inactivité du rendez-vous expirait à 600s sur un tour structured actif car seul le transcript Claude servait de signe de vie; nouveau drain_with_readiness_announcements_and_activity_outcome exposant un jeton monotone AtomicU64 incrémenté par chaque ReplyEvent non terminal du tap send_with_tap, passé au watchdog via AskWatchdogActivity pour réarmer la fenêtre quelle que soit l'adapter; la sonde transcript devient fallback externe; QA verte: orchestrator_service 81/81, rendezvous 5/5, mcp_server 40/40, backend 87+7 ignored, cargo fmt --check; scripts/ hors périmètre)

This commit is contained in:
2026-09-08 20:29:48 +02:00
parent 4484a02b16
commit 77169101cb
5 changed files with 277 additions and 28 deletions

View File

@ -23,7 +23,8 @@ pub use session_limit::{AgentResumer, SessionLimitService, RESUME_PROMPT};
pub use structured::{
drain_reply_stream_with_readiness, drain_with_readiness,
drain_with_readiness_and_announcements, drain_with_readiness_and_announcements_outcome,
drain_with_readiness_outcome, send_blocking, AnnouncementPublisher, TurnOutcome,
drain_with_readiness_announcements_and_activity_outcome, drain_with_readiness_outcome,
send_blocking, AnnouncementPublisher, TurnOutcome,
};
pub use catalogue::{

View File

@ -12,6 +12,7 @@
//! DRY : un seul chemin de lecture (le flux). `send_blocking` n'est qu'un *consom-
//! mateur* du même flux que la cellule chat utilise pour le rendu incrémental.
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
@ -203,15 +204,49 @@ pub async fn drain_with_readiness_and_announcements_outcome(
agent: RuntimeAgentKey,
announcements: Option<AnnouncementPublisher>,
) -> Result<TurnOutcome, AgentSessionError> {
let Some(publisher) = announcements else {
if announcements.is_none() {
return drain_with_readiness_outcome(session, prompt, timeout, mediator, agent).await;
};
}
drain_with_readiness_announcements_and_activity_outcome(
session,
prompt,
timeout,
mediator,
agent,
announcements,
None,
)
.await
}
/// Variante du drain d'annonces qui expose aussi un jeton monotone d'activité live.
///
/// Le jeton avance pour chaque [`ReplyEvent`] non terminal reçu via
/// [`AgentSession::send_with_tap`], donc pendant que `send_with_tap` travaille encore
/// et avant que son flux batch final soit disponible. Il permet au watchdog du
/// rendez-vous de distinguer un tour structured actif d'un vrai silence, quel que
/// soit le provider. `Final` reste exclusivement traité par le drain normal.
pub async fn drain_with_readiness_announcements_and_activity_outcome(
session: &dyn AgentSession,
prompt: &str,
timeout: Option<Duration>,
mediator: &dyn InputMediator,
agent: RuntimeAgentKey,
announcements: Option<AnnouncementPublisher>,
activity: Option<Arc<AtomicU64>>,
) -> Result<TurnOutcome, AgentSessionError> {
let (tap_tx, tap_rx) = std::sync::mpsc::channel();
let live_publisher = publisher.clone();
let live_publisher = announcements.clone();
let live_pump = std::thread::spawn(move || {
for event in tap_rx {
live_publisher.publish_event(&event);
if !matches!(event, ReplyEvent::Final { .. }) {
if let Some(activity) = &activity {
activity.fetch_add(1, Ordering::Relaxed);
}
}
if let Some(publisher) = &live_publisher {
publisher.publish_event(&event);
}
}
});
@ -230,7 +265,9 @@ pub async fn drain_with_readiness_and_announcements_outcome(
if !matches!(event, ReplyEvent::Final { .. }) {
mediator.mark_alive(agent);
}
publisher.publish_event(event);
if let Some(publisher) = &announcements {
publisher.publish_event(event);
}
},
|signal| {
if signal == ReadinessSignal::TurnEnded {