feat(orchestrator): durcir le rendez-vous inter-agents + backstop no-reply (Finding A)

Corrige le wedge survenant lorsqu'un agent délégué ne rappelle pas idea_reply :
le rendez-vous ask_agent ne reste plus bloqué indéfiniment.

Lot 1 — durcissement du préambule de délégation :
- input/mod.rs : delegation_preamble renforcé (obligation explicite idea_reply).
- agent/lifecycle.rs : préambule injecté conditionné à mcp_enabled.

Lot 2 — peuplement du prompt_ready_pattern :
- agent/catalogue.rs : .with_prompt_ready_pattern("? for shortcuts") sur le
  profil Claude + tests de seed, pour calibrer la détection de grâce.

Lot 3 — backstop timeout serveur :
- mcp/jsonrpc.rs : code d'erreur typé RENDEZVOUS_NO_REPLY (-32001).
- mcp/server.rs : mapping timeout → erreur typée, with_ask_rendezvous_timeout
  promu + resolve_ask_rendezvous_timeout (configurable).
- app-tauri/state.rs : lecture env IDEA_ASK_RENDEZVOUS_TIMEOUT_MS.
- tests/mcp_server.rs mis à jour, fix d'un test input flaky.

Propagation overlay (référence des profils) :
- agent/catalogue.rs : overlay_reference_defaults + reference_index + tests.
- store/profile.rs : ReferenceBackfillProfileStore + tests.
- app-tauri/state.rs : wrap au composition root.
- exports mod.rs / lib.rs (application + infrastructure).

Tests réels verts : application 494, app-tauri 235, infrastructure 248,
mcp_server 22/22, 0 échec. Validation e2e LIVE (rebuild AppImage) en attente
avant merge vers develop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 23:46:48 +02:00
parent 47b3806d32
commit c5e54935b8
13 changed files with 414 additions and 29 deletions

View File

@ -875,10 +875,21 @@ impl MediatedInbox {
/// The `[IdeA · tâche de <requester> · ticket <id>]` header is the protocol signal
/// (cadrage B-5, agent context) that marks the message as an IdeA delegation the
/// target must answer via `idea_reply` — echoing `ticket` for multi-thread
/// correlation — rather than as a free-text human prompt. The raw `task` follows on
/// the next line so the agent reads the request unchanged.
/// correlation — rather than as a free-text human prompt.
///
/// A one-line **imperative reminder** follows the header (durcissement
/// comportemental, finding A) : the single most common wedge is a target that ends
/// its turn with a *prose* answer and never calls `idea_reply`, leaving the asker
/// parked. Reminding it inline — on every delegated task, even trivial ones — attacks
/// that behavioural cause at the central point where the turn is composed (no per-agent
/// duplication). The raw `task` then follows so the agent reads the request unchanged.
fn delegation_preamble(requester: &str, ticket: TicketId, task: &str) -> String {
format!("[IdeA · tâche de {requester} · ticket {ticket}]\n{task}")
format!(
"[IdeA · tâche de {requester} · ticket {ticket}]\n\
⚠️ Termine IMPÉRATIVEMENT ce tour par un appel `idea_reply(ticket=\"{ticket}\", \
result=…)`, même pour une réponse triviale. Ne réponds JAMAIS uniquement en prose \
dans le terminal : sans `idea_reply`, l'agent demandeur reste bloqué.\n{task}"
)
}
fn write_delegation_chunks(
@ -1425,8 +1436,14 @@ mod tests {
inbox.enqueue(a, ticket(10, &task));
// Attendre le SUBMIT final (`\r`, écrit après le délai), pas un simple seuil de
// writes : le nombre de chunks de contenu dépend de la taille du payload (préambule
// + tâche), donc seul le `\r` terminal est un point d'arrêt stable.
assert!(
wait_until(|| pty.writes.lock().unwrap().len() >= 4),
wait_until(|| {
let w = pty.writes.lock().unwrap();
w.len() >= 4 && w.last().map(Vec::as_slice) == Some(b"\r".as_slice())
}),
"long headless delivery writes multiple chunks followed by submit"
);
let writes = pty.writes.lock().unwrap().clone();