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:
@ -23,11 +23,12 @@ use domain::ports::{
|
||||
use domain::profile::{McpConfigStrategy, StructuredAdapter};
|
||||
use domain::sandbox::{compile_sandbox_plan, SandboxContext, SandboxPlan};
|
||||
use domain::{
|
||||
Agent, AgentId, AgentManifest, AgentOrigin, AgentProfile, ContextInjection, ConversationId,
|
||||
ConversationParty, DomainEvent, EffectivePermissions, Handoff, HandoffStore, ManifestEntry,
|
||||
MarkdownDoc, MemoryIndexEntry, MemoryType, NodeId, PermissionProjector, ProfileId, Project,
|
||||
ProjectPath, ProjectedFile, ProjectionContext, ProjectorKey, ProviderSessionStore, PtySize,
|
||||
SessionId, SessionKind, SessionStatus, Skill, TerminalSession,
|
||||
bound_handoff_summary, Agent, AgentId, AgentManifest, AgentOrigin, AgentProfile,
|
||||
ContextInjection, ConversationId, ConversationParty, DomainEvent, EffectivePermissions,
|
||||
Handoff, HandoffStore, ManifestEntry, MarkdownDoc, MemoryIndexEntry, MemoryType, NodeId,
|
||||
PermissionProjector, ProfileId, Project, ProjectPath, ProjectedFile, ProjectionContext,
|
||||
ProjectorKey, ProviderSessionStore, PtySize, SessionId, SessionKind, SessionStatus, Skill,
|
||||
TerminalSession, HANDOFF_SUMMARY_MAX_CHARS,
|
||||
};
|
||||
|
||||
use domain::live_state::WorkStatus;
|
||||
@ -1318,7 +1319,15 @@ impl LaunchAgent {
|
||||
let conversation = ConversationId::from_uuid(uuid::Uuid::parse_str(raw).ok()?);
|
||||
let store = provider.handoff_store_for(root)?;
|
||||
// Toute erreur de lecture/désérialisation ⇒ pas d'injection (best-effort).
|
||||
store.load(conversation).await.ok().flatten()
|
||||
// Borne défensive LS5 : un `handoff.md` legacy non borné (écrit avant LS5) est
|
||||
// ramené sous `HANDOFF_SUMMARY_MAX_CHARS` **avant** d'entrer dans le contexte
|
||||
// composé — sans migration de données, idempotent et best-effort.
|
||||
store
|
||||
.load(conversation)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|h| bound_handoff_summary(h, HANDOFF_SUMMARY_MAX_CHARS))
|
||||
}
|
||||
|
||||
/// Résout **best-effort** l'aperçu live-state des **autres** agents (lot LS4) à
|
||||
@ -3738,6 +3747,56 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compose_with_normal_handoff_is_byte_identical_pre_and_post_ls5_bound() {
|
||||
// ZÉRO RÉGRESSION (LS5) : pour un handoff sous le plafond, le seul changement
|
||||
// LS5 du chemin d'injection — `bound_handoff_summary` dans `resolve_handoff` —
|
||||
// est l'identité (cf. domaine `bound_under_limit_is_unchanged`). Le document
|
||||
// composé est donc OCTET-IDENTIQUE avant/après LS5. Assertion stricte.
|
||||
let memory = [mem(
|
||||
"git-optional",
|
||||
"Git optionnel",
|
||||
"git reste un simple tool",
|
||||
MemoryType::Project,
|
||||
)];
|
||||
let normal = handoff(
|
||||
"**Objectif :** livrer LS5\n\n- **Prompt:** a\n- **Response:** b",
|
||||
Some("livrer LS5"),
|
||||
);
|
||||
assert!(
|
||||
normal.summary_md.chars().count() <= HANDOFF_SUMMARY_MAX_CHARS,
|
||||
"pré-condition : handoff sous le plafond"
|
||||
);
|
||||
|
||||
// « Avant LS5 » : le composeur reçoit le handoff tel quel (aucune borne).
|
||||
let pre = compose_convention_file(
|
||||
"/root",
|
||||
"",
|
||||
"# Persona",
|
||||
&[],
|
||||
&memory,
|
||||
Some(&normal),
|
||||
&[],
|
||||
true,
|
||||
);
|
||||
// « Après LS5 » : le handoff passe d'abord par la borne universelle.
|
||||
let bounded = bound_handoff_summary(normal.clone(), HANDOFF_SUMMARY_MAX_CHARS);
|
||||
let post = compose_convention_file(
|
||||
"/root",
|
||||
"",
|
||||
"# Persona",
|
||||
&[],
|
||||
&memory,
|
||||
Some(&bounded),
|
||||
&[],
|
||||
true,
|
||||
);
|
||||
assert_eq!(
|
||||
pre, post,
|
||||
"handoff normal ⇒ document composé octet-identique"
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// État du projet (lot LS4) — live-state preview section in the composer.
|
||||
// The composer is pure: it renders the already-distilled `InjectedLiveRow`s
|
||||
|
||||
Reference in New Issue
Block a user