Lot 7 : boucle du contrat du mode Code tenue par la façade

Port de codeVerifyLoop (rust/loki-rs/src/chat/verify.rs) : à la fin d'un
tour de build en mode Code, relances du builder sur les critères encore
ouverts, passe de vérification sur un contexte isolé, corrections, abandon
dit dans le fil après deux corrections — mêmes deltas que le Go (role,
verify_done, mesures ctx_isolated), relances et corrections rangées dans
l'historique, vérifications non.

Le Go garde les critères (l'outil criteria s'y exécute) : la façade les lit
en base (chat/crit:<id>) et reçoit le contexte isolé du vérificateur par
POST /internal/v1/turn/verifier (prompt du rôle et faits de la machine,
outils fermés, rôle verifier, seul autorisé à marquer passed). Toute
compaction en mode Code rend l'état des critères au résumé, comme
compactMessages.

generate : vue du tour factorisée (turn_view), question à l'utilisateur
suivie (une question posée suspend la vérification).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
This commit is contained in:
Claude committed 2026-10-15 12:02:36 +00:00
1 parent a739534104
commit daa2e7e4c1
10 files changed
+961 -37

No files matched your search

+93 -35
View File
@@ -18,11 +18,13 @@
//! enregistrée, et la file — abandonnée après un stop, sinon le message
//! suivant part (ou la notification Web Push s'il n'y en a pas).
//!
//! Écarts assumés (restent au Go, à porter au lot 7) : boucle de
//! vérification du mode Code (`codeVerifyLoop`), critères rendus pour la
//! compaction, PROJ_SNAPSHOT et KEEP_TURN_IMAGES (refusés par
//! `/turn/view`), préchauffage, télémétrie, motifs `retry_patterns`
//! personnalisés.
//! Mode Code : la boucle du contrat (vérification isolée, relances et
//! corrections du builder) suit le tour, avant la compaction de fin
//! ([`super::verify`]) ; toute compaction rend l'état des critères au résumé.
//!
//! Écarts assumés (restent au Go) : PROJ_SNAPSHOT et KEEP_TURN_IMAGES
//! (refusés par `/turn/view`), préchauffage, télémétrie, motifs
//! `retry_patterns` personnalisés.
use std::collections::VecDeque;
use std::future::Future;
@@ -146,7 +148,7 @@ struct Queue {
recent: VecDeque<String>,
}
fn obj(k: &str, v: impl Into<Value>) -> Delta {
pub(super) fn obj(k: &str, v: impl Into<Value>) -> Delta {
let mut d = Map::new();
d.insert(k.into(), v.into());
d
@@ -155,13 +157,13 @@ fn obj(k: &str, v: impl Into<Value>) -> Delta {
type BoxFut = Pin<Box<dyn Future<Output = ()> + Send>>;
/// Ce qu'un tour lit une fois au départ.
struct TurnEnv {
cfg: std::collections::HashMap<String, String>,
ep: super::wire::ChatEndpoint,
echo: super::wire::EchoPolicy,
ccfg: CompactConfig,
port: u16,
summarizer: EngineSummarizer,
pub(super) struct TurnEnv {
pub(super) cfg: std::collections::HashMap<String, String>,
pub(super) ep: super::wire::ChatEndpoint,
pub(super) echo: super::wire::EchoPolicy,
pub(super) ccfg: CompactConfig,
pub(super) port: u16,
pub(super) summarizer: EngineSummarizer,
}
/// Le chat tenu par la façade : la discussion ouverte, ses tours et sa file.
@@ -423,12 +425,17 @@ impl ChatService {
let conv = self.sessions.conv();
conv.append_delta(epoch, obj("compacting", true));
let recall = StoreRecall(self.shared.store.clone());
let criteria = if caps.code {
super::host::live_criteria(&self.shared.store, &conv.id()).await
} else {
String::new()
};
let c = Compactor {
cfg: ccfg,
summarizer,
recall: &recall,
memory: &self.shared.compact_memory,
criteria: "",
criteria: &criteria,
};
let (compacted, changed) = c.compact_messages_noted(msgs, caps, opt).await;
conv.append_delta(epoch, obj("compacting", false));
@@ -456,8 +463,33 @@ impl ChatService {
Some(compacted)
}
/// `/internal/v1/turn/view` : tête (prompt du preset, consignes,
/// projet), préambule et outils du tour ; `Err` : le message à journaliser.
pub(super) async fn turn_view(
&self,
caps: &Value,
) -> Result<(Vec<Message>, String, Vec<Tool>), String> {
match self
.shared
.store
.internal_post("/internal/v1/turn/view", &json!({ "caps": caps }))
.await
{
Ok((s, v)) if s.as_u16() == 200 => Ok((
serde_json::from_value::<Vec<Message>>(v["head"].clone()).unwrap_or_default(),
v["prefix"].as_str().unwrap_or("").to_string(),
serde_json::from_value::<Vec<Tool>>(v["tools"].clone()).unwrap_or_default(),
)),
Ok((_, v)) => Err(v["error"]
.as_str()
.unwrap_or("vue du tour refusée")
.to_string()),
Err(e) => Err(e),
}
}
/// Configuration, point de chat et résumeur d'un tour (lus une fois).
async fn turn_env(&self) -> TurnEnv {
pub(super) async fn turn_env(&self) -> TurnEnv {
let store = &self.shared.store;
let cfg = store.config().await;
let local_key = store.get_str(BK_STATE, "api_key").await;
@@ -603,7 +635,7 @@ impl ChatService {
) {
let turn_start = Instant::now();
let conv = self.sessions.conv().clone();
let store = self.shared.store.clone();
let env = self.turn_env().await;
let TurnEnv {
cfg,
ep,
@@ -611,7 +643,15 @@ impl ChatService {
ccfg,
port,
summarizer,
} = self.turn_env().await;
} = &env;
let (cfg, ep, echo, ccfg, port, summarizer) = (
cfg.clone(),
ep.clone(),
echo.clone(),
ccfg.clone(),
*port,
summarizer.clone(),
);
let local = !ep.external;
let conv_id = conv.id();
let agent = prep.cap("Agent");
@@ -680,23 +720,9 @@ impl ChatService {
// Vue d'envoi : tête du Go (prompt du preset, consignes, projet),
// l'historique sans bloc <context_update>, le préambule.
let view = store
.internal_post("/internal/v1/turn/view", &json!({ "caps": prep.turn_caps }))
.await;
let (head, prefix, tools) = match view {
Ok((s, v)) if s.as_u16() == 200 => (
serde_json::from_value::<Vec<Message>>(v["head"].clone()).unwrap_or_default(),
v["prefix"].as_str().unwrap_or("").to_string(),
serde_json::from_value::<Vec<Tool>>(v["tools"].clone()).unwrap_or_default(),
),
other => {
let e = match other {
Ok((_, v)) => v["error"]
.as_str()
.unwrap_or("vue du tour refusée")
.to_string(),
Err(e) => e,
};
let (head, prefix, tools) = match self.turn_view(&prep.turn_caps).await {
Ok(v) => v,
Err(e) => {
conv.append_delta(epoch, obj("error", e));
self.finish(epoch, &cancel, lease, turn_start).await;
return;
@@ -760,6 +786,7 @@ impl ChatService {
let mut final_echo: Option<ReasoningEcho> = None;
let mut saw_usage = false;
let mut turn_peak = 0i64;
let mut asked = false;
let mut cb = |ev: StreamEvent| -> bool {
match ev {
StreamEvent::Err(e) => {
@@ -793,6 +820,8 @@ impl ChatService {
);
}
StreamEvent::Ask(a) => {
// Question structurée (outil ask) : carte à boutons.
asked = true;
conv.append_delta(
epoch,
obj("ask", serde_json::to_value(&a).unwrap_or_default()),
@@ -852,7 +881,7 @@ impl ChatService {
let msgs = st.messages.clone();
(ctx_now(st, &msgs), st.gen_peak, msgs)
});
let Some((mut ctx_used, peak, msgs)) = after else {
let Some((mut ctx_used, mut peak, mut msgs)) = after else {
lease.release().await;
return; // reset pendant le tour : il a déjà enregistré le fil vide
};
@@ -868,6 +897,35 @@ impl ChatService {
ctx_used = est;
}
// Boucle du contrat (mode Code) : vérification indépendante des
// critères, corrections, re-vérification — AVANT de rendre la main
// (verify.rs). Rien hors mode Code ou sans critères.
if !cancel.is_cancelled() {
let pe = super::verify::PassEnv {
env: &env,
turn_caps: &prep.turn_caps,
temperature,
reasoning: &reasoning,
ccaps,
conv_id: &conv_id,
};
let mut run = super::verify::PassRun {
lease: &mut lease,
cancel: &cancel,
cancel_tx: &cancel_tx,
};
self.code_verify_loop(&pe, epoch, asked, &mut run).await;
let after = conv.with_state_if(epoch, |st| {
let m = st.messages.clone();
(ctx_now(st, &m), st.gen_peak, m)
});
let Some(a) = after else {
lease.release().await;
return; // reset pendant la vérification
};
(ctx_used, peak, msgs) = a;
}
// Compaction de fin de tour : la jauge retombe tout de suite, le tour
// suivant démarre avec de la marge. Pas après un stop.
if !cancel.is_cancelled()