supercode-harness 0.5.45

The optional native Volter Harness agent and tool harness
Documentation
//! P4b (COMPOSABLE-HARNESS-DESIGN.md §5.2 "P4", §1.6/§3.1, catalog §4a
//! "Turn/step usage records surfaced per turn"): persisted per-turn
//! token/usage records. [`crate::AgentEvent::Usage`] already streams this
//! data live (UX-23); this module makes it DURABLE session data — a typed,
//! serde-round-trippable record, not a lossy display-only channel (§1.13's
//! lossless/sidecar discipline: this is typed session data, exactly like
//! [`crate::reduce::ReductionLog`], not a text notice).

use serde::{Deserialize, Serialize};

use crate::provider::Usage;

/// One model round-trip's token accounting, with the context a bare
/// [`Usage`] lacks: which turn it was and which model served it (D9 "model
/// provenance" — obligation 6's "served-model provenance" row). Deliberately
/// flat/typed (not a formatted string) so it survives a save/load round trip
/// byte-for-byte in the fields that matter, and so a future reader (a
/// `doctor`/`inspect stats` command, a cost dashboard) can aggregate it
/// without re-parsing text.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct UsageRecord {
    /// 0-based index of the model round-trip this record covers (one per
    /// [`crate::AgentEvent::TurnCompleted`], the same cadence `UX-23`
    /// already uses).
    pub turn: usize,
    /// The model id that served this turn (`Config.model`, or the
    /// mid-session-switched model once obligation 10 lands — recorded
    /// per-turn rather than once per session so a handoff is visible in the
    /// log, not just implied).
    pub model: String,
    /// Input tokens.
    #[serde(default)]
    pub prompt_tokens: u64,
    /// Output tokens.
    #[serde(default)]
    pub completion_tokens: u64,
    /// Total tokens (provider-reported; not always `prompt + completion`
    /// exactly, so kept as its own field rather than derived).
    #[serde(default)]
    pub total_tokens: u64,
    /// Prompt tokens served from the provider's cache (B7), if reported.
    #[serde(default)]
    pub cached_tokens: Option<u64>,
    /// Unix-ms wall-clock time the record was created.
    #[serde(default)]
    pub timestamp_ms: i64,
    /// BP-7 (catalog §4a "Per-turn cost/usage accounting" — the COST half
    /// the row's semantics name alongside tokens): this round-trip's dollar
    /// cost at the model's resolved [`crate::pricing::ModelPrice`].
    /// `None` when this build cannot price the model — never a guess, and
    /// never zero standing in for "unknown".
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cost_usd: Option<f64>,
    /// BP-13 (D9 "Model-served-vs-requested provenance"): the model the
    /// PROVIDER reported as having produced the response, when it reported
    /// one at all. [`Self::model`] above is what was REQUESTED; these two
    /// can genuinely differ (a gateway resolving a floating name to a dated
    /// snapshot, a routed tier, a fallback hop), and a record that carries
    /// only the request can never show it. `None` means the provider said
    /// nothing — never "they matched".
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub served_model: Option<String>,
}

impl UsageRecord {
    /// Build a record from a provider [`Usage`] plus the per-turn context a
    /// bare `Usage` doesn't carry.
    pub fn from_usage(turn: usize, model: &str, usage: &Usage, timestamp_ms: i64) -> UsageRecord {
        UsageRecord {
            turn,
            model: model.to_string(),
            prompt_tokens: usage.prompt_tokens,
            completion_tokens: usage.completion_tokens,
            total_tokens: usage.total_tokens,
            cached_tokens: usage.prompt_tokens_details.map(|d| d.cached_tokens),
            timestamp_ms,
            cost_usd: None,
            served_model: None,
        }
    }

    /// BP-7: the same record with `cost_usd` filled in from `price`, or
    /// unchanged when the model has no resolvable price.
    pub fn priced(mut self, price: Option<crate::pricing::ModelPrice>) -> UsageRecord {
        self.cost_usd = price.map(|p| p.cost_usd(self.prompt_tokens, self.completion_tokens));
        self
    }

    /// BP-13: attach the provider-reported serving model. `None` leaves the
    /// record saying nothing about it, which is the honest reading when the
    /// response carried no `model` field.
    pub fn with_served_model(mut self, served: Option<String>) -> UsageRecord {
        self.served_model = served;
        self
    }

    /// BP-13: whether the model that answered differs from the one asked
    /// for. `false` when the provider reported nothing — an unknown is not
    /// a divergence.
    pub fn diverged(&self) -> bool {
        self.served_model
            .as_deref()
            .is_some_and(|served| served != self.model)
    }
}

/// Serialize `records` as JSONL (one [`UsageRecord`] per line) — the same
/// shape every other append-log in this crate uses (the transcript, the
/// sidecar). Never fails on an empty slice (produces an empty string).
pub fn to_jsonl(records: &[UsageRecord]) -> crate::Result<String> {
    let mut out = String::new();
    for r in records {
        out.push_str(&serde_json::to_string(r).map_err(crate::Error::Decode)?);
        out.push('\n');
    }
    Ok(out)
}

/// Parse a JSONL usage log back into records — the exact inverse of
/// [`to_jsonl`]. Blank lines are skipped (tolerates a trailing newline or
/// hand-edited whitespace); a malformed line is a hard error (unlike the
/// hooks/sidecar "fail open" posture — a corrupt usage record should be
/// visible, not silently dropped, since it's accounting data).
pub fn from_jsonl(text: &str) -> crate::Result<Vec<UsageRecord>> {
    let mut out = Vec::new();
    for line in text.lines() {
        let line = line.trim();
        if line.is_empty() {
            continue;
        }
        out.push(serde_json::from_str(line).map_err(crate::Error::Decode)?);
    }
    Ok(out)
}