Fusion de main : conversation Rust par défaut, télémétrie du tour

main a reçu en parallèle la télémétrie du tour tenu par la façade
(/internal/v1/turn/perf), les tickets « en vol » du moteur et la
conversation tenue par la façade par défaut (LOKI_CHAT=go pour revenir
au Go). Le lot 29 faisait la même chose par /internal/v1/perf/record :
un seul chemin est gardé, celui de main.

- TurnHost::perf_record (main) remplace TurnHost::perf ; la nature est
  posée par l'hôte (verify pour la passe isolée seulement, main pour les
  corrections du builder, comme le Go).
- Les résumés de compaction passent par le même point d'entrée, nature
  compact : chat/perf.rs ne garde que le relevé de la réponse entière
  (perfRecFromWire) et l'envoi.
- La sonde du gabarit reste à la façade (lot 23) : /internal/v1/turn/perf
  ne la lance plus côté Go.
- facade_perf.go retiré ; ses cas (perte, intercalé, alerte, preset
  externe, incomplète, GET) rejoignent web_internal_turnperf_test.go.
- README : LOKI_CHAT défaut Rust, avec ce que les lots 19, 23, 29 et 30
  y ajoutent.

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-16 01:53:53 +00:00
commit 89ed527c96
20 files changed
+713 -457

No files matched your search

+41 -13
View File
@@ -125,7 +125,7 @@ pub struct FakeGo {
/// pour une action donnée.
pub service_os: Mutex<Vec<String>>,
pub service_os_fail: Mutex<Option<(String, String)>>,
/// /internal/v1/perf/record reçus (corps entiers).
/// /internal/v1/turn/perf reçus (corps entiers).
pub perf_records: Mutex<Vec<serde_json::Value>>,
}
@@ -502,14 +502,6 @@ async fn handle(st: Arc<FakeGo>, req: Request<Incoming>) -> Response<Full<Bytes>
}
resp(200, r#"{"ran":true,"error":""}"#)
}
"/internal/v1/perf/record" => {
let body = req.into_body().collect().await.unwrap().to_bytes();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap_or_default();
let mut log = st.perf_records.lock().unwrap();
log.push(v);
let out = serde_json::json!({"seq": log.len(), "lost": 12, "lost_after": "task", "alert": false});
resp(200, out.to_string())
}
"/internal/v1/tune/context" => {
let v = serde_json::json!({
"busy": *st.tune_ctx_busy.lock().unwrap(),
@@ -673,15 +665,51 @@ async fn handle(st: Arc<FakeGo>, req: Request<Incoming>) -> Response<Full<Bytes>
"/internal/v1/oai/begin" => {
let body = req.into_body().collect().await.unwrap().to_bytes();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
st.oai_log.lock().unwrap().push(format!(
"begin read={} side={} post_v1={}",
v["read"], v["side"], v["post_v1"]
));
if let Some(conv) = v["main_conv"].as_str() {
st.oai_log
.lock()
.unwrap()
.push(format!("begin main_conv={conv}"));
} else {
st.oai_log.lock().unwrap().push(format!(
"begin read={} side={} post_v1={}",
v["read"], v["side"], v["post_v1"]
));
}
if let Some(r) = st.refuse.lock().unwrap().clone() {
return resp(200, serde_json::json!({ "refuse": r }).to_string());
}
resp(200, r#"{"ticket":7}"#)
}
"/internal/v1/oai/served" => {
let body = req.into_body().collect().await.unwrap().to_bytes();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
st.oai_log
.lock()
.unwrap()
.push(format!("served {}", v["ticket"]));
resp(204, "")
}
"/internal/v1/turn/perf" => {
let body = req.into_body().collect().await.unwrap().to_bytes();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
st.oai_log.lock().unwrap().push(format!(
"perf kind={} conv={} iter={} complete={} prefix={}",
v["kind"].as_str().unwrap_or(""),
v["conv"].as_str().unwrap_or(""),
v["iter"],
v["complete"],
v["prefix"].as_array().map_or(0, Vec::len),
));
// Comme le Go : la nature reçue (un tour sans nature).
let kind = v["kind"]
.as_str()
.filter(|k| !k.is_empty())
.unwrap_or("main");
let out = serde_json::json!({"kind": kind, "lost": 3, "lost_after": "tâche", "lost_alert": true});
st.perf_records.lock().unwrap().push(v);
resp(200, out.to_string())
}
"/internal/v1/oai/end" => {
let body = req.into_body().collect().await.unwrap().to_bytes();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
+55 -9
View File
@@ -692,6 +692,7 @@ async fn tour_complet_par_le_go() {
kwargs: Default::default(),
},
queued: Box::new(Vec::new),
perf_kind: "main",
};
let bash = Tool {
kind: "function".into(),
@@ -711,7 +712,6 @@ async fn tour_complet_par_le_go() {
echo: Default::default(),
compact: CompactConfig::default(),
patterns: shared.patterns.clone(),
perf_kind: "main",
};
let question = Message {
role: "user".into(),
@@ -767,15 +767,15 @@ async fn tour_complet_par_le_go() {
// discussion ; la première, sans usage, reste incomplète.
let perfs = go.perf_records.lock().unwrap().clone();
assert_eq!(perfs.len(), 2);
assert_eq!(perfs[0]["rec"]["complete"], false);
assert_eq!(perfs[0]["complete"], false);
assert_eq!(perfs[0]["prefix"], serde_json::json!([]));
let r1 = &perfs[1]["rec"];
let r1 = &perfs[1];
assert_eq!(
(
r1["conv"].clone(),
r1["kind"].clone(),
r1["iter"].clone(),
r1["total"].clone()
r1["stats"]["prompt_tokens_total"].clone()
),
(
serde_json::json!("c7"),
@@ -794,7 +794,10 @@ async fn tour_complet_par_le_go() {
_ => None,
})
.unwrap();
assert_eq!((last.lost, last.lost_after.as_str()), (Some(12), "task"));
assert_eq!(
(last.kind.as_str(), last.lost, last.lost_after.as_str()),
("main", Some(3), "tâche")
);
}
#[tokio::test]
@@ -942,6 +945,47 @@ async fn tour_de_bout_en_bout() {
// Bail rendu, fin signalée, discussion enregistrée.
assert_eq!(*go.lease.lock().unwrap(), None);
let id = svc.sessions.conv().id();
// Chaque requête au moteur local est comptée en vol par le Go (ticket
// ouvert, servi, rendu), et sa complétion rangée dans la télémétrie ;
// la copie finale des stats porte ce que le Go en dit.
until(|| {
go.oai_log
.lock()
.unwrap()
.iter()
.filter(|l| l.starts_with("end"))
.count()
== 2
})
.await;
let log = go.oai_log.lock().unwrap().clone();
let want_begin = format!("begin main_conv={id}");
assert_eq!(
log.iter().filter(|l| **l == want_begin).count(),
2,
"{log:?}"
);
assert_eq!(
log.iter().filter(|l| *l == "served 7").count(),
2,
"{log:?}"
);
// Appel d'outil sans usage ni timings : complétion incomplète (comme le
// Go : ni empreintes) ; la réponse finale, avec usage, complète. La
// sonde du gabarit reste à la façade (`sonde_de_gabarit_apres_un_tour`).
for want in [
format!("perf kind=main conv={id} iter=0 complete=false prefix=0"),
format!("perf kind=main conv={id} iter=1 complete=true prefix=4"),
] {
assert!(log.contains(&want), "{want} absent de {log:?}");
}
let finals: Vec<_> = deltas(&svc, "stats")
.into_iter()
.filter(|s| s["lost_alert"] == true)
.collect();
assert_eq!(finals.len(), 1, "stats finales sans les marques du Go");
assert_eq!(finals[0]["lost"], 3);
assert_eq!(finals[0]["kind"], "main");
let body = opened(&go, BK_CHAT, &format!("conv:{id}")).unwrap();
assert_eq!(decode_state(&body).messages.len(), 4);
// Réessai du même envoi : reconnu.
@@ -1024,6 +1068,7 @@ async fn hote_et_sonde_de_gabarit() {
kwargs: Default::default(),
},
queued: Box::new(Vec::new),
perf_kind: "main",
}
};
let bash = Tool {
@@ -1848,10 +1893,11 @@ async fn telemetrie_des_resumes() {
let perfs = go.perf_records.lock().unwrap().clone();
assert_eq!(perfs.len(), 2, "la réponse illisible n'est pas rangée");
assert_eq!(
perfs[0]["rec"],
perfs[0],
serde_json::json!({ "kind": "compact", "conv": "c9", "iter": 0, "complete": true,
"total": 3000, "new": 3000, "pp_ms": 1500.0, "gen": 200, "tg_tps": 40.0 })
"external": false, "prefix": [],
"stats": { "prompt_tokens_total": 3000, "prompt_tokens": 3000, "prompt_ms": 1500.0,
"gen_tokens": 200, "gen_per_second": 40.0 } })
);
assert_eq!(perfs[0]["prefix"], serde_json::json!([]));
assert_eq!(perfs[1]["rec"]["complete"], false);
assert_eq!(perfs[1]["complete"], false);
}
-1
View File
@@ -195,7 +195,6 @@ async fn replay(fx: Fixture) -> Result<(), String> {
echo,
patterns: RetryPatterns::builtin(),
cfg,
perf_kind: "main",
};
let host = ParityHost {
replies: Mutex::new(fx.replies.into()),