feat(orchestrator): modèle de désignation d'orchestrateur + sink de diagnostic

Introduit le modèle AgentManifest { version, entries, orchestrator } et la
garde d'écriture directe may_write_directly(..., &OrchestratorDesignation) :
seul l'orchestrateur désigné peut écrire directement, les autres passent par
le rendez-vous médié. Câble la désignation à travers domain → application →
infrastructure → app-tauri (context_guard, service, lifecycle, ports).

Ajoute crates/application/src/diag.rs : sink de diagnostic best-effort, sans
dépendance, qui miroite les traces du rendez-vous inter-agents de
l'orchestrateur vers un fichier de log persistant (utile au lancement via
AppImage où stderr est jeté), avec la même discipline « zéro dépendance,
ne casse jamais le rendez-vous ».

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 08:56:17 +02:00
parent 40982d44da
commit 287681c198
57 changed files with 1758 additions and 420 deletions

View File

@ -92,10 +92,7 @@ impl ResetTimeFormat {
Self::EpochSeconds => c.parse::<i64>().ok().map(|s| s.saturating_mul(1000)),
Self::EpochMillis => c.parse::<i64>().ok(),
Self::Iso8601 => timeparse::parse_rfc3339_to_ms(c),
Self::RelativeSeconds => c
.parse::<f64>()
.ok()
.map(|d| now_ms + (d * 1000.0) as i64),
Self::RelativeSeconds => c.parse::<f64>().ok().map(|d| now_ms + (d * 1000.0) as i64),
Self::RelativeMillis => c.parse::<i64>().ok().map(|d| now_ms + d),
Self::WallClock => {
let (h, m, s) = timeparse::parse_wall_clock(c)?;
@ -153,7 +150,11 @@ impl RateLimitParser {
.or_else(|| name.parse::<usize>().ok().and_then(|i| caps.get(i)))?;
self.time_format.resolve(raw.as_str(), now_ms)
});
Some(SessionLimit::new(resets_at_ms, now_ms, RateLimitSource::Pattern))
Some(SessionLimit::new(
resets_at_ms,
now_ms,
RateLimitSource::Pattern,
))
}
}
@ -204,7 +205,9 @@ mod tests {
#[test]
fn detect_match_without_reset_capture_has_no_time() {
let parser = RateLimitParser::new(&pattern("rate limit reached", None, None)).unwrap();
let limit = parser.detect("oops: rate limit reached!", NOW).expect("détecté");
let limit = parser
.detect("oops: rate limit reached!", NOW)
.expect("détecté");
assert_eq!(limit.resets_at_ms, None);
assert_eq!(limit.detected_at_ms, NOW);
assert_eq!(limit.source, RateLimitSource::Pattern);
@ -340,9 +343,13 @@ mod tests {
Some("epoch_s"),
))
.unwrap();
let a = parser.detect("resets at 1700000000", NOW).expect("1er détecté");
let a = parser
.detect("resets at 1700000000", NOW)
.expect("1er détecté");
assert!(parser.detect("rien ici", NOW).is_none());
let b = parser.detect("resets at 1700000000", NOW).expect("2e détecté");
let b = parser
.detect("resets at 1700000000", NOW)
.expect("2e détecté");
assert_eq!(a.resets_at_ms, b.resets_at_ms);
assert_eq!(a.resets_at_ms, Some(1_700_000_000_000));
}
@ -368,7 +375,10 @@ mod tests {
let profile = base_profile()
.with_structured_adapter(StructuredAdapter::Claude)
.with_rate_limit_pattern(pattern("rate limit", None, None));
assert!(!applies(&profile), "un agent structuré détecte par le niveau 1");
assert!(
!applies(&profile),
"un agent structuré détecte par le niveau 1"
);
}
#[test]