feat(orchestrator): backstop no-reply du rendez-vous inter-agents
Remédiation du wedge persistant après échec live du fix Finding A (77e62e5).
Détecte la fin de tour d'un agent sollicité qui n'a pas appelé idea_reply et
débloque l'agent demandeur au lieu de le laisser en attente indéfinie.
Ajoute le suivi de tour côté inspector Claude (claude_turn_watcher) et la
résolution des chemins de session (claude_paths), câblés dans le rendez-vous
idea_ask_agent ⇄ idea_reply.
Build workspace vert, suite complète verte, zéro warning.
Backstop NON prouvé levé en live : merge develop interdit tant que la levée
du wedge n'est pas validée en conditions réelles.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -177,9 +177,9 @@ impl LivenessStrategy {
|
||||
/// Motif déclaratif de détection d'une **limite de session/débit** pour un agent
|
||||
/// **PTY/TUI sans adapter structuré** (ARCHITECTURE §21, niveau 2 de détection).
|
||||
///
|
||||
/// Donnée **pure** (pas de code par CLI — Open/Closed, §9), calquée sur la
|
||||
/// philosophie de [`AgentProfile::prompt_ready_pattern`] mais **plus riche** : là où
|
||||
/// le retour-de-prompt est une simple sous-chaîne littérale, la limite de session a
|
||||
/// Donnée **pure** (pas de code par CLI — Open/Closed, §9), un motif déclaratif
|
||||
/// littéral **plus riche** qu'une simple sous-chaîne : là où un sigil de prompt
|
||||
/// serait une simple sous-chaîne littérale, la limite de session a
|
||||
/// besoin d'**extraire une heure de reset** dans la sortie. Le domaine **ne stocke
|
||||
/// que la donnée** (chaînes) ; le **moteur regex et le parsing d'heure vivent en
|
||||
/// infrastructure** (composant `RateLimitParser`, dépendance `regex` ajoutée au seul
|
||||
@ -578,37 +578,6 @@ pub struct AgentProfile {
|
||||
/// sérialisation : un profil sans MCP sérialise exactement comme avant.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub mcp: Option<McpCapability>,
|
||||
/// Motif de **retour-de-prompt** (cadrage « conversation par paire » §6, lot C5).
|
||||
///
|
||||
/// Donnée **déclarative** (pas de code par CLI — Open/Closed) : un motif **littéral**
|
||||
/// recherché par sous-chaîne dans le flux de sortie PTY du tour courant. Quand il
|
||||
/// apparaît, IdeA considère l'agent **revenu au prompt** et le marque `Idle`
|
||||
/// (`InputMediator::mark_idle`), ce qui fait avancer sa file. C'est l'un des deux
|
||||
/// signaux du « double signal OR » (l'autre étant un `idea_reply` explicite) ; le
|
||||
/// **premier** des deux qui arrive libère le tour.
|
||||
///
|
||||
/// **Choix tranché : littéral, pas regex.** Une sous-chaîne littérale est
|
||||
/// déterministe, sans dépendance (`regex`), suffisante pour un sigil de prompt
|
||||
/// stable (ex. `"\n> "`), et ne risque pas le faux-positif d'un motif regex mal
|
||||
/// échappé présent dans la sortie. Un moteur regex pourra être ajouté plus tard
|
||||
/// comme variante déclarative (Open/Closed) si le besoin se confirme.
|
||||
///
|
||||
/// **Rang (chantier readiness/heartbeat, lot 1) : signal de repli n°3.** Depuis
|
||||
/// l'introduction de la fin-de-tour structurée ([`crate::ports::ReplyEvent::Final`]
|
||||
/// ⇒ [`crate::readiness::ReadinessSignal::TurnEnded`], signal n°1) et du signal
|
||||
/// explicite `idea_reply` (n°2), ce sniff littéral est **rétrogradé** au rang de
|
||||
/// repli : il ne sert plus que pour les agents **TUI/PTY sans adapter structuré**.
|
||||
/// Conservé tel quel pour la rétro-compat (jamais supprimé).
|
||||
///
|
||||
/// `None` (défaut, et valeur des profils existants) ⇒ **aucune** détection par
|
||||
/// motif : l'agent ne repasse `Idle` que sur signal explicite (`idea_reply`) ou via
|
||||
/// le garde-fou du timeout par tour. Conforme au fallback « en cas de doute → reste
|
||||
/// `Busy` mais la file continue d'accepter » (jamais de faux `Idle`).
|
||||
///
|
||||
/// `skip_serializing_if = Option::is_none` ⇒ **zéro régression** de sérialisation :
|
||||
/// un profil sans motif sérialise exactement comme avant.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub prompt_ready_pattern: Option<String>,
|
||||
/// Réglages de **vivacité** (readiness/heartbeat, chantier lot 1). `None` (défaut,
|
||||
/// et valeur des profils existants) ⇒ comportement actuel. **Lot 1** : champ
|
||||
/// présent mais **non consommé** (place ménagée pour les seuils de stagnation /
|
||||
@ -804,7 +773,6 @@ impl AgentProfile {
|
||||
session,
|
||||
structured_adapter: None,
|
||||
mcp: None,
|
||||
prompt_ready_pattern: None,
|
||||
liveness: None,
|
||||
rate_limit_pattern: None,
|
||||
submit_sequence: None,
|
||||
@ -831,15 +799,6 @@ impl AgentProfile {
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder : fixe le motif de **retour-de-prompt** (§6, lot C5) et renvoie le
|
||||
/// profil. Laisse [`AgentProfile::new`] stable (zéro régression d'appel) : les
|
||||
/// profils sans détection de prompt ne l'appellent simplement pas.
|
||||
#[must_use]
|
||||
pub fn with_prompt_ready_pattern(mut self, pattern: impl Into<String>) -> Self {
|
||||
self.prompt_ready_pattern = Some(pattern.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder : fixe la [`LivenessStrategy`] (readiness/heartbeat, lot 1) et renvoie
|
||||
/// le profil. Laisse [`AgentProfile::new`] stable (zéro régression d'appel) : les
|
||||
/// profils sans réglage de vivacité ne l'appellent simplement pas.
|
||||
@ -1117,25 +1076,14 @@ mod mcp_tests {
|
||||
);
|
||||
}
|
||||
|
||||
// -- Lot C5 : prompt_ready_pattern (détection retour-de-prompt) --------------
|
||||
// -- Backstop no-reply : le champ `promptReadyPattern` a été retiré ----------
|
||||
|
||||
#[test]
|
||||
fn profile_default_has_no_prompt_ready_pattern() {
|
||||
// Existing profiles (built via `new`) carry no pattern ⇒ no false Idle.
|
||||
assert!(profile_without_mcp().prompt_ready_pattern.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_without_prompt_pattern_omits_key_in_json() {
|
||||
let json = serde_json::to_string(&profile_without_mcp()).expect("serialise");
|
||||
assert!(
|
||||
!json.contains("promptReadyPattern"),
|
||||
"a profile without a prompt pattern must NOT serialise the key (zero regression); got: {json}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_json_without_prompt_pattern_deserialises_to_none() {
|
||||
fn legacy_json_with_obsolete_prompt_ready_pattern_is_ignored() {
|
||||
// Le watcher prompt-ready PTY est supprimé (remplacé par le TurnWatcher
|
||||
// transcript). Un `profiles.json` antérieur portant encore la clé doit
|
||||
// continuer à se désérialiser (pas de `deny_unknown_fields`) : la clé obsolète
|
||||
// est simplement ignorée — zéro régression de lecture des profils existants.
|
||||
let legacy = r#"{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"name": "Dev",
|
||||
@ -1143,22 +1091,11 @@ mod mcp_tests {
|
||||
"args": [],
|
||||
"contextInjection": { "strategy": "conventionFile", "target": "CLAUDE.md" },
|
||||
"detect": null,
|
||||
"cwdTemplate": "{agentRunDir}"
|
||||
"cwdTemplate": "{agentRunDir}",
|
||||
"promptReadyPattern": "? for shortcuts"
|
||||
}"#;
|
||||
let profile: AgentProfile = serde_json::from_str(legacy).expect("legacy deserialise");
|
||||
assert!(profile.prompt_ready_pattern.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_prompt_ready_pattern_sets_and_round_trips() {
|
||||
let profile = profile_without_mcp().with_prompt_ready_pattern("\n> ");
|
||||
assert_eq!(profile.prompt_ready_pattern.as_deref(), Some("\n> "));
|
||||
|
||||
let json = serde_json::to_string(&profile).expect("serialise");
|
||||
assert!(json.contains("promptReadyPattern"), "key present: {json}");
|
||||
let back: AgentProfile = serde_json::from_str(&json).expect("deserialise");
|
||||
assert_eq!(profile, back);
|
||||
assert_eq!(back.prompt_ready_pattern.as_deref(), Some("\n> "));
|
||||
assert_eq!(profile.command, "claude");
|
||||
}
|
||||
|
||||
// -- §20 : submit_sequence / submit_delay_ms (portail d'écriture) ------------
|
||||
|
||||
Reference in New Issue
Block a user