feat(domain,infra,app): borne summary_md du handoff + durcissement seam LLM (LS5)

Clôt le must-have perf du handoff :
- domain : HANDOFF_SUMMARY_MAX_CHARS + bound_handoff_summary (helpers privés), re-export lib.
- infrastructure : TURN_LINE_MAX_CHARS + élision dans render_turn (summarizer), re-exports.
- application : borne entre fold et save (conversation/record), borne défensive
  dans resolve_handoff (agent/lifecycle) + module test.
- seam LLM prêt mais NON activé (aucun LLM câblé).
- tests (QA, verts) : conversation_log, conversation_record, agent_lifecycle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 12:29:44 +02:00
parent 533a9c57f3
commit 13a953cb05
10 changed files with 673 additions and 16 deletions

View File

@ -49,7 +49,7 @@ mod summarizer;
pub use handoff::FsHandoffStore;
pub use providers::FsProviderSessionStore;
pub use summarizer::{HeuristicHandoffSummarizer, WINDOW};
pub use summarizer::{HeuristicHandoffSummarizer, TURN_LINE_MAX_CHARS, WINDOW};
/// Dossier `.ideai/` à la racine d'un project root.
pub(crate) const IDEAI_DIR: &str = ".ideai";

View File

@ -51,6 +51,14 @@ pub const WINDOW: usize = 20;
/// Longueur maximale (en caractères) de l'objectif extrait d'un premier prompt.
const OBJECTIVE_MAX_CHARS: usize = 200;
/// Longueur maximale (en caractères) du **texte aplati** d'un tour rendu sur une ligne
/// (lot LS5). C'est le correctif central du trou de perf : sans borne, un gros
/// `Response` produit une ligne géante qui gonfle le `summary_md` (et donc le contexte
/// injecté à chaque lancement). Au-delà, le texte est tronqué avec une élision `…` ; le
/// préfixe `- **role:**` reste **intact** (la ligne reste reparsable). Borne **publique**
/// pour que l'agent Test la vérifie sans la deviner.
pub const TURN_LINE_MAX_CHARS: usize = 240;
/// Préfixe de la ligne d'objectif dans le `summary_md` (sert au rendu **et** au reparse).
const OBJECTIVE_PREFIX: &str = "**Objectif :** ";
@ -80,7 +88,9 @@ fn render_turn(turn: &ConversationTurn) -> String {
TurnRole::Response => "Response",
TurnRole::ToolActivity => "Tool",
};
let flat = flatten(&turn.text);
// Borne LS5 : le texte aplati est tronqué à `TURN_LINE_MAX_CHARS` (élision `…`) pour
// éviter qu'un gros Response ne produise une ligne géante. Le préfixe reste intact.
let flat = elide(&flatten(&turn.text), TURN_LINE_MAX_CHARS);
format!("- **{label}:** {flat}")
}
@ -89,6 +99,17 @@ fn flatten(text: &str) -> String {
text.split_whitespace().collect::<Vec<_>>().join(" ")
}
/// Tronque `text` à `max_chars` caractères (char-boundary safe), en ajoutant une
/// élision `…` quand il a effectivement été coupé. En deçà, renvoie le texte tel quel.
fn elide(text: &str, max_chars: usize) -> String {
if text.chars().count() <= max_chars {
text.to_owned()
} else {
let kept: String = text.chars().take(max_chars).collect();
format!("{kept}")
}
}
/// Reparse les lignes de tours déjà rendues dans un `summary_md` précédent.
///
/// Ne récupère que les lignes-tours (préfixe `- **`), en ignorant la ligne d'objectif et

View File

@ -39,7 +39,8 @@ pub mod timeparse;
pub use clock::SystemClock;
pub use conversation::InMemoryConversationRegistry;
pub use conversation_log::{
FsConversationLog, FsHandoffStore, FsProviderSessionStore, HeuristicHandoffSummarizer, WINDOW,
FsConversationLog, FsHandoffStore, FsProviderSessionStore, HeuristicHandoffSummarizer,
TURN_LINE_MAX_CHARS, WINDOW,
};
pub use eventbus::TokioBroadcastEventBus;
pub use fileguard::RwFileGuard;