supercode-harness 0.4.3

The optional native Supercode agent and tool harness
Documentation
//! Typed schemas for the on-disk session formats.
//!
//! The loaders in [`crate::session`] used to pluck fields out of untyped
//! `serde_json::Value`s, which meant anything we didn't explicitly look for was
//! silently lost — and *invisible*. These modules give each format a typed
//! representation instead, with two deliberate escape hatches that turn "what
//! are we missing?" into a mechanical question:
//!
//! - every discriminated enum has a `#[serde(other)]` `Unknown` variant, so an
//!   unmodeled record/block/payload **type** deserializes into a named bucket
//!   instead of erroring or vanishing; and
//! - every record struct carries a flattened `extra: ExtraFields` map, so an
//!   unmodeled **field** is captured rather than dropped.
//!
//! The [`crate::audit`] module walks a corpus, deserializes into these types,
//! and reports every `Unknown` and every non-empty `extra` — an evidence-based
//! map of exactly where our coverage ends.

pub mod claude_code;
pub mod codex;

use std::collections::BTreeMap;

use serde::Deserialize;

/// Catch-all for object fields a struct does not explicitly model.
///
/// `#[serde(flatten)]` this into any record struct: fields the struct names are
/// consumed normally, and everything else lands here where the audit can see
/// it. An empty map means we model the record fully.
pub type ExtraFields = BTreeMap<String, serde_json::Value>;

/// A content block that may carry text, used by both formats' message bodies.
/// Unknown block types are preserved by tag in [`ContentBlock::Unknown`].
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentBlock {
    /// Plain text.
    Text {
        /// The text.
        #[serde(default)]
        text: String,
    },
    /// Anthropic reasoning block (not replayable across providers).
    Thinking {
        /// The (possibly empty) thinking text.
        #[serde(default)]
        thinking: String,
    },
    /// Anthropic redacted reasoning.
    RedactedThinking {
        /// Opaque payload.
        #[serde(default)]
        data: Option<String>,
    },
    /// An assistant tool call (Anthropic shape).
    ToolUse {
        /// Provider call id.
        id: String,
        /// Tool name.
        name: String,
        /// Arguments object.
        #[serde(default)]
        input: serde_json::Value,
    },
    /// A tool result (Anthropic shape).
    ToolResult {
        /// The id of the tool_use this answers.
        #[serde(default)]
        tool_use_id: Option<String>,
        /// String or array-of-blocks content.
        #[serde(default)]
        content: serde_json::Value,
    },
    /// An image block (multimodal).
    Image {
        /// The image source descriptor.
        #[serde(default)]
        source: serde_json::Value,
    },
    /// Codex input text block.
    InputText {
        /// The text.
        #[serde(default)]
        text: String,
    },
    /// Codex output text block.
    OutputText {
        /// The text.
        #[serde(default)]
        text: String,
    },
    /// Codex input image block.
    InputImage {
        /// Opaque image reference.
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// PARITY-11 (provenance P011): a Claude provider-routing note —
    /// real shape `{"type":"fallback","from":{"model":..},"to":{"model":..}}`,
    /// a mid-generation model swap (e.g. an overloaded model falling back to
    /// another). `session.rs`'s loader folds it into a short bracketed text
    /// marker rather than dropping it.
    Fallback {
        /// Anything (`from`/`to` model descriptors).
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// Any block type we do not model yet. The tag is recovered via
    /// `ContentBlock::unknown_tag` from the captured fields.
    #[serde(other)]
    Unknown,
}

impl ContentBlock {
    /// Returns the static discriminant name for a modeled block, or `None` for
    /// [`ContentBlock::Unknown`].
    pub fn tag(&self) -> Option<&'static str> {
        Some(match self {
            ContentBlock::Text { .. } => "text",
            ContentBlock::Thinking { .. } => "thinking",
            ContentBlock::RedactedThinking { .. } => "redacted_thinking",
            ContentBlock::ToolUse { .. } => "tool_use",
            ContentBlock::ToolResult { .. } => "tool_result",
            ContentBlock::Image { .. } => "image",
            ContentBlock::InputText { .. } => "input_text",
            ContentBlock::OutputText { .. } => "output_text",
            ContentBlock::InputImage { .. } => "input_image",
            ContentBlock::Fallback { .. } => "fallback",
            ContentBlock::Unknown => return None,
        })
    }

    /// Best-effort flattened text from a block (text-bearing variants only).
    pub fn as_text(&self) -> Option<&str> {
        match self {
            ContentBlock::Text { text }
            | ContentBlock::InputText { text }
            | ContentBlock::OutputText { text } => Some(text),
            _ => None,
        }
    }
}

/// Recover the `type` discriminant of an arbitrary JSON content block, even one
/// that deserialized as [`ContentBlock::Unknown`] — used by the audit to name
/// the unknown.
pub fn raw_block_tag(v: &serde_json::Value) -> Option<String> {
    v.get("type")
        .and_then(serde_json::Value::as_str)
        .map(str::to_string)
}