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

+81 -38
View File
@@ -23,15 +23,16 @@
//! `cb` (comme le `ChatCallback` Go) ; le tour rend ses messages à ajouter à
//! l'historique ([`TurnResult::extra`]).
//!
//! Télémétrie : chaque complétion lue est rangée (`perfRecord`, par
//! [`TurnHost::perf`]) ; la dernière statistique d'une complétion complète
//! porte la nature et la perte de cache calculée par le Go.
//! Télémétrie : chaque complétion lue est rangée par le Go (`perfRecord`, par
//! [`TurnHost::perf_record`]) ; la dernière statistique d'une complétion
//! complète porte la nature et la perte de cache qu'il a calculées.
//!
//! Non portés (clés opt-in, ou machinerie du moteur local restée au Go) :
//! KEEP_TURN_IMAGES, PROJ_SNAPSHOT, SIDE_SLOT (slot et rejeux « cache KV
//! plein »), SLOT_PERSIST, préchauffage, comptage des requêtes en vol du
//! moteur. Avec l'une de ces clés, le Go garde la conversation
//! (`facadeChatBlocker`, décidé au démarrage).
//! plein »), SLOT_PERSIST, préchauffage. Avec l'une de ces clés, le Go garde
//! la conversation (`facadeChatBlocker`, décidé au démarrage). Le comptage
//! des requêtes en vol du moteur passe par le Go (l'hôte autour de
//! [`TurnHost::send`]).
use std::collections::HashMap;
use std::future::Future;
@@ -57,14 +58,13 @@ use super::nudges::{
MAX_NUDGES, THINK_NUDGE_FIRST, THINK_NUDGE_STUCK, append_nudge, budget_nudge, dedupable_tool,
log_budget, repeated_call_result, steer_system, tool_result_preview,
};
use super::perf::PerfRec;
use super::stream::{
CacheHintFn, MAX_PATTERN_RETRIES, PreflightFn, RetryPatterns, StreamConfig, StreamParser,
call_args, retry_corrective, tool_call_label,
};
use super::types::{
AskEvent, DiffLine, Message, ReasoningEcho, StreamEvent, Tool, ToolCall, ToolCallFunc,
ToolUsedEvent,
AskEvent, DiffLine, Message, ReasoningEcho, StatsEvent, StreamEvent, Tool, ToolCall,
ToolCallFunc, ToolUsedEvent,
};
use super::wire::{
ChatEndpoint, ChatPayloadOpts, EchoPolicy, TOOLS_OFF_HINT, TurnReasoning, build_chat_payload,
@@ -185,20 +185,56 @@ pub trait TurnHost: Send + Sync {
fn cache_hint(&self) -> Option<CacheHintFn> {
None
}
/// Range la télémétrie d'une complétion (`perfRecord`), rattachée à la
/// discussion du tour ; rend la perte de cache calculée et l'alerte.
fn perf(
&self,
_rec: &super::perf::PerfRec,
_prefix: &[u64],
_external: bool,
) -> impl Future<Output = super::perf::PerfNoted> + Send {
async { super::perf::PerfNoted::default() }
}
/// Garde d'écriture du mode Code (`codeWriteGuard`).
fn preflight(&self) -> Option<PreflightFn> {
None
}
/// Télémétrie d'une complétion (`perfRecord`) : la rend au Go, qui la
/// range dans l'anneau de `/api/perf/summary` et dit quoi poser sur la
/// copie finale des stats (nature, jetons perdus, alerte).
fn perf_record(&self, _rec: PerfRec) -> impl Future<Output = PerfMarks> + Send {
async { PerfMarks::default() }
}
}
/// Une complétion telle que la boucle l'a vue, pour la télémétrie.
#[derive(Debug, Clone, PartialEq, Default)]
pub struct PerfRec {
/// Rang de la complétion dans le tour (`perfIter`).
pub iter: i64,
pub complete: bool,
pub external: bool,
pub stats: StatsEvent,
/// Empreintes cumulées des messages envoyés ([`perf_prefix`]), si complète.
pub prefix: Vec<u64>,
}
/// Ce que la télémétrie pose sur la copie finale des stats.
#[derive(Debug, Clone, PartialEq, Default)]
pub struct PerfMarks {
pub kind: String,
pub lost: Option<i64>,
pub lost_after: String,
pub lost_alert: bool,
}
/// `perfPrefix` : empreinte FNV-1a 64 cumulée de chaque message envoyé (son
/// JSON suivi d'un octet nul). Comparée seulement aux empreintes des envois
/// précédents de la même discussion, toutes calculées ici.
pub fn perf_prefix(msgs: &[Message]) -> Vec<u64> {
const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const PRIME: u64 = 0x0000_0100_0000_01b3;
let fnv = |h: u64, b: u8| (h ^ u64::from(b)).wrapping_mul(PRIME);
let mut h = OFFSET;
let mut out = Vec::with_capacity(msgs.len());
for m in msgs {
for b in serde_json::to_vec(m).unwrap_or_default() {
h = fnv(h, b);
}
h = fnv(h, 0);
out.push(h);
}
out
}
/// Ce que le tour sait avant de commencer (lu UNE fois).
@@ -216,8 +252,6 @@ pub struct TurnConfig {
pub echo: EchoPolicy,
pub compact: CompactConfig,
pub patterns: Arc<RetryPatterns>,
/// Nature des complétions pour la télémétrie (`perfMain`, `perfVerify`).
pub perf_kind: &'static str,
}
/// Fin du tour : messages à ajouter à l'historique, et l'erreur s'il y en a
@@ -364,7 +398,7 @@ struct Loop {
echo_tpl_retry: bool,
tool_runs: i64,
budget_nudges: i64,
/// Complétions rangées dans la télémétrie (`perfIter`).
/// `perfIter` : complétions de ce tour, complètes ou non.
perf_iter: i64,
}
@@ -433,9 +467,8 @@ pub async fn run_turn<H: TurnHost>(
let calls_on = !tc.tools.is_empty() && !s.disable_tools && !s.tool_choice_none;
let pol = turn_echo_policy(ep.external || s.echo_off_turn, || tc.echo.clone());
host.prepare_images(&messages).await;
let (body, echo_sent, sent_prefix) = {
let (body, echo_sent, prefix) = {
let (sent, echo_sent) = wire_messages(&messages, s.tool_choice_none, &pol);
let sent_prefix = super::perf::prefix(&sent);
let payload = build_chat_payload(
ep,
&sent,
@@ -454,7 +487,7 @@ pub async fn run_turn<H: TurnHost>(
(
serde_json::to_vec(&payload).unwrap_or_default(),
echo_sent,
sent_prefix,
perf_prefix(&sent),
)
};
let t_req = Instant::now();
@@ -629,28 +662,38 @@ pub async fn run_turn<H: TurnHost>(
// de plus ne part vers l'écran. La complétion coupée reste dans la
// télémétrie, incomplète.
if stopped {
let rec =
PerfRec::from_stats(tc.perf_kind, "", s.perf_iter, false, &Default::default());
host.perf(&rec, &[], ep.external).await;
host.perf_record(PerfRec {
iter: s.perf_iter,
external: ep.external,
..Default::default()
})
.await;
return TurnResult { extra, err: None };
}
let out = parser.finish(&mut emit);
// Télémétrie : une complétion coupée avant son chunk final n'a ni cache
// ni total fiables — rangée incomplète, sans calcul de perte.
let complete = out.is_complete(read_err.is_some());
let rec = PerfRec::from_stats(tc.perf_kind, "", s.perf_iter, complete, &out.stats);
let noted = host
.perf(&rec, if complete { &sent_prefix } else { &[] }, ep.external)
// Télémétrie (perf_log.go) : une complétion coupée avant son chunk
// final est rangée comme incomplète, sans calcul de perte.
let marks = host
.perf_record(PerfRec {
iter: s.perf_iter,
complete,
external: ep.external,
stats: out.stats.clone(),
prefix: if complete { prefix } else { Vec::new() },
})
.await;
s.perf_iter += 1;
if complete {
// Dernier événement de la complétion : copie complète des stats,
// avec la perte de cache vue par la télémétrie.
// avec ce que la télémétrie en dit (nature, perte, alerte).
let mut st = out.stats.clone();
st.kind = tc.perf_kind.into();
st.lost = noted.lost;
st.lost_after = noted.lost_after;
st.lost_alert = noted.alert;
if !marks.kind.is_empty() {
st.kind = marks.kind;
}
st.lost = marks.lost;
st.lost_after = marks.lost_after;
st.lost_alert = marks.lost_alert;
cb(StreamEvent::Stats(st));
// Sonde de gabarit, en tâche de fond et après coup : rien n'en
// revient dans la requête suivante.