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

+98 -15
View File
@@ -33,7 +33,7 @@ use super::compact::{
use super::engine::{EngineHttp, classify, friendly_error};
use super::persist::{Persister, StoreSink, ToolResJob};
use super::stream::{CacheHintFn, PreflightFn, RetryPatterns};
use super::turn::{CompactPhase, Reply, SendError, ToolOutcome, TurnHost};
use super::turn::{CompactPhase, PerfMarks, PerfRec, Reply, SendError, ToolOutcome, TurnHost};
use super::types::{AskEvent, DiffLine, Message, Tool};
use super::wire::{
ChatEndpoint, ChatPayloadOpts, EchoRefusals, EffortFallbacks, IMG_REF_SCHEME,
@@ -212,8 +212,14 @@ impl EngineSummarizer {
Err(_) => false,
};
if decoded && let Some((store, conv)) = &self.perf {
let rec = super::perf::PerfRec::from_wire(super::perf::KIND_COMPACT, conv, &raw);
super::perf::record(store, &rec, &[], self.ep.external).await;
let (complete, stats) = super::perf::wire_stats(&raw);
let rec = PerfRec {
complete,
external: self.ep.external,
stats,
..Default::default()
};
super::perf::record(store, super::perf::KIND_COMPACT, conv, &rec).await;
}
out
}
@@ -331,6 +337,65 @@ pub struct GoTurnHost {
pub compact: TurnCompact,
/// Messages mis en file par l'utilisateur pendant le tour.
pub queued: Box<dyn Fn() -> Vec<Message> + Send + Sync>,
/// Nature des complétions pour la télémétrie : `main` (tour de la
/// discussion), `verify` (passe de vérification du mode Code).
pub perf_kind: &'static str,
}
/// Rend le ticket « en vol » au Go quand la réponse du moteur est lue ou
/// abandonnée (`engineRequestBeginSide` … `end`).
struct GateTicket {
store: Store,
ticket: u64,
}
impl Drop for GateTicket {
fn drop(&mut self) {
let (store, ticket) = (self.store.clone(), self.ticket);
if let Ok(rt) = tokio::runtime::Handle::try_current() {
rt.spawn(async move {
let _ = store
.internal_json(
hyper::Method::POST,
"/internal/v1/oai/end",
Some(&json!({ "ticket": ticket })),
)
.await;
});
}
}
}
impl GoTurnHost {
/// Ouvre un ticket « en vol » auprès du Go : le slot de la discussion
/// n'est pas effacé pendant la requête, et un préchauffage est annulé.
/// `None` si le Go ne répond pas — la requête part quand même.
async fn gate_begin(&self) -> Option<GateTicket> {
let main_conv = if self.perf_kind == super::perf::KIND_MAIN {
self.conv_id.as_str()
} else {
""
};
match self
.shared
.store
.internal_json(
hyper::Method::POST,
"/internal/v1/oai/begin",
Some(&json!({ "main_conv": main_conv })),
)
.await
{
Ok(v) => v["ticket"].as_u64().map(|ticket| GateTicket {
store: self.shared.store.clone(),
ticket,
}),
Err(e) => {
eprintln!("[loki-rs] moteur : requête non comptée en vol ({e})");
None
}
}
}
}
/// `toolResID` : « <discussion>.<24 hex> ».
@@ -389,10 +454,35 @@ pub fn see_image_message(label: &str, img: Value) -> Message {
impl TurnHost for GoTurnHost {
async fn send(&self, ep: &ChatEndpoint, body: Vec<u8>) -> Result<Reply, SendError> {
// Moteur local : requête comptée en vol jusqu'à la fin de lecture du
// flux (le ticket voyage avec le corps). Rien pour une API externe.
let gate = if ep.external {
None
} else {
self.gate_begin().await
};
match self.shared.http.post(&ep.url, &ep.key, body).await {
Ok(resp) => {
let status = resp.status().as_u16();
let body = resp.bytes_stream().map(|r| r.map_err(|e| e.to_string()));
if status == 200
&& let Some(g) = &gate
{
// Le slot porte désormais cette requête (engineServed,
// engineMarkMain pour une étape de la discussion).
let _ = self
.shared
.store
.internal_json(
hyper::Method::POST,
"/internal/v1/oai/served",
Some(&json!({ "ticket": g.ticket })),
)
.await;
}
let body = resp.bytes_stream().map(move |r| {
let _keep = &gate;
r.map_err(|e| e.to_string())
});
Ok(Reply {
status,
body: Box::pin(body),
@@ -408,17 +498,6 @@ impl TurnHost for GoTurnHost {
}
}
async fn perf(
&self,
rec: &super::perf::PerfRec,
prefix: &[u64],
external: bool,
) -> super::perf::PerfNoted {
let mut rec = rec.clone();
rec.conv = self.conv_id.clone();
super::perf::record(&self.shared.store, &rec, prefix, external).await
}
async fn exec_tool(&self, name: &str, args: &Map<String, Value>, label: &str) -> ToolOutcome {
// Outils fichiers et shell : ici (lot 7), l'état partagé restant au Go.
if crate::tools::is_local(name) {
@@ -571,6 +650,10 @@ impl TurnHost for GoTurnHost {
None
}
async fn perf_record(&self, rec: PerfRec) -> PerfMarks {
super::perf::record(&self.shared.store, self.perf_kind, &self.conv_id, &rec).await
}
fn preflight(&self) -> Option<PreflightFn> {
None
}