supercode-harness 0.4.17

The optional native Supercode 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)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::provider::PromptTokensDetails;

    #[test]
    fn from_usage_carries_every_field() {
        let usage = Usage {
            prompt_tokens: 100,
            completion_tokens: 20,
            total_tokens: 120,
            prompt_tokens_details: Some(PromptTokensDetails { cached_tokens: 40 }),
        };
        let r = UsageRecord::from_usage(2, "anthropic/claude-opus-4-8", &usage, 1_700_000_000_000);
        assert_eq!(r.turn, 2);
        assert_eq!(r.model, "anthropic/claude-opus-4-8");
        assert_eq!(r.prompt_tokens, 100);
        assert_eq!(r.completion_tokens, 20);
        assert_eq!(r.total_tokens, 120);
        assert_eq!(r.cached_tokens, Some(40));
        assert_eq!(r.timestamp_ms, 1_700_000_000_000);
    }

    #[test]
    fn priced_fills_cost_only_when_a_price_resolves() {
        let usage = Usage {
            prompt_tokens: 1_000_000,
            completion_tokens: 0,
            total_tokens: 1_000_000,
            prompt_tokens_details: None,
        };
        let record = UsageRecord::from_usage(0, "anthropic/claude-opus-4-8", &usage, 0);
        assert_eq!(record.cost_usd, None, "unpriced until `priced` is called");
        let priced = record
            .clone()
            .priced(crate::pricing::built_in(&record.model));
        assert_eq!(
            priced.cost_usd,
            Some(crate::pricing_ref::REF_INPUT_PER_MTOK)
        );
        assert_eq!(record.priced(None).cost_usd, None);
    }

    #[test]
    fn from_usage_with_no_cache_details_is_none() {
        let usage = Usage {
            prompt_tokens: 10,
            completion_tokens: 5,
            total_tokens: 15,
            prompt_tokens_details: None,
        };
        let r = UsageRecord::from_usage(0, "m", &usage, 0);
        assert_eq!(r.cached_tokens, None);
    }

    #[test]
    fn jsonl_round_trip_is_lossless() {
        let records = vec![
            UsageRecord {
                turn: 0,
                model: "anthropic/claude-opus-4-8".to_string(),
                prompt_tokens: 1000,
                completion_tokens: 50,
                total_tokens: 1050,
                cached_tokens: Some(200),
                timestamp_ms: 1_700_000_000_000,
                cost_usd: Some(0.018_75),
                served_model: Some("anthropic/claude-opus-4-8-20260101".to_string()),
            },
            UsageRecord {
                turn: 1,
                model: "anthropic/claude-haiku-4-5".to_string(),
                prompt_tokens: 2000,
                completion_tokens: 75,
                total_tokens: 2075,
                cached_tokens: None,
                timestamp_ms: 1_700_000_005_000,
                cost_usd: None,
                served_model: None,
            },
        ];
        let jsonl = to_jsonl(&records).unwrap();
        let round_tripped = from_jsonl(&jsonl).unwrap();
        assert_eq!(records, round_tripped);
    }

    /// BP-13: the served model rides the record when the provider reported
    /// one, and its ABSENCE is not read as agreement.
    #[test]
    fn served_model_records_divergence_and_never_infers_agreement() {
        let usage = Usage::default();
        let asked = UsageRecord::from_usage(0, "vendor/floating", &usage, 0);
        assert_eq!(asked.served_model, None);
        assert!(!asked.diverged(), "an unknown is not a divergence");
        let served = asked
            .clone()
            .with_served_model(Some("vendor/floating-20260101".to_string()));
        assert!(served.diverged());
        let same = UsageRecord::from_usage(0, "vendor/m", &usage, 0)
            .with_served_model(Some("vendor/m".to_string()));
        assert!(!same.diverged());
        // A record written before this field existed still parses.
        let old = r#"{"turn":0,"model":"m","prompt_tokens":1}"#;
        assert_eq!(from_jsonl(old).unwrap()[0].served_model, None);
    }

    #[test]
    fn empty_records_round_trip_to_empty() {
        assert_eq!(to_jsonl(&[]).unwrap(), "");
        assert_eq!(from_jsonl("").unwrap(), Vec::<UsageRecord>::new());
    }

    #[test]
    fn from_jsonl_skips_blank_lines() {
        let text = "\n\n";
        assert_eq!(from_jsonl(text).unwrap(), Vec::<UsageRecord>::new());
    }
}