supercode-harness 0.4.3

The optional native Supercode agent and tool harness
Documentation
//! Typed schema for Codex rollout JSONL (`~/.codex/sessions/.../rollout-*.jsonl`).
//!
//! Each line is an envelope `{timestamp, type, payload}`. The `type` selects
//! the envelope kind; for `response_item` the inner `payload.type` selects the
//! conversation item. Discriminants we don't model land in `Unknown` variants
//! so the audit can enumerate them; unmodeled fields land in `extra` maps.

use serde::Deserialize;

use super::ExtraFields;

/// One line of a Codex rollout file.
#[derive(Debug, Clone, Deserialize)]
pub struct CodexLine {
    /// Envelope timestamp.
    #[serde(default)]
    pub timestamp: Option<String>,
    /// The envelope, discriminated by its `type` field.
    #[serde(flatten)]
    pub record: CodexRecord,
}

/// A Codex envelope.
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum CodexRecord {
    /// Session header (id, cwd, base instructions, …).
    SessionMeta {
        /// The header payload.
        payload: SessionMeta,
    },
    /// Per-turn context (carries the model, sandbox/approval policy, …).
    TurnContext {
        /// The turn-context payload (loosely modeled).
        payload: TurnContext,
    },
    /// A canonical conversation item.
    ResponseItem {
        /// The conversation payload, discriminated by its own `type`.
        payload: ResponseItem,
    },
    /// A UI event echo (token counts, task lifecycle, streamed renderings).
    /// These duplicate `response_item` content and are not part of the
    /// canonical conversation, but we name the payload type for completeness.
    EventMsg {
        /// The event payload.
        payload: EventMsg,
    },
    /// A whole-history compaction record (replaces earlier turns).
    Compacted {
        /// The compaction payload.
        #[serde(default)]
        payload: serde_json::Value,
    },
    /// Any envelope type we do not model yet.
    #[serde(other)]
    Unknown,
}

impl CodexRecord {
    /// The static discriminant name, or `None` for [`CodexRecord::Unknown`].
    pub fn tag(&self) -> Option<&'static str> {
        Some(match self {
            CodexRecord::SessionMeta { .. } => "session_meta",
            CodexRecord::TurnContext { .. } => "turn_context",
            CodexRecord::ResponseItem { .. } => "response_item",
            CodexRecord::EventMsg { .. } => "event_msg",
            CodexRecord::Compacted { .. } => "compacted",
            CodexRecord::Unknown => return None,
        })
    }
}

/// Codex `session_meta` payload.
#[derive(Debug, Clone, Deserialize)]
pub struct SessionMeta {
    /// Session id.
    #[serde(default)]
    pub id: Option<String>,
    /// Working directory.
    #[serde(default)]
    pub cwd: Option<String>,
    /// Base/system instructions (string or `{text}`).
    #[serde(default)]
    pub base_instructions: Option<serde_json::Value>,
    /// Model provider, e.g. `openai`.
    #[serde(default)]
    pub model_provider: Option<String>,
    /// Anything not modeled above.
    #[serde(flatten)]
    pub extra: ExtraFields,
}

/// Codex `turn_context` payload (loosely modeled — many policy fields).
#[derive(Debug, Clone, Deserialize)]
pub struct TurnContext {
    /// The model for this turn.
    #[serde(default)]
    pub model: Option<String>,
    /// Working directory for this turn.
    #[serde(default)]
    pub cwd: Option<String>,
    /// Everything else (approval policy, sandbox policy, effort, …).
    #[serde(flatten)]
    pub extra: ExtraFields,
}

/// A canonical conversation item (`response_item` payload).
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ResponseItem {
    /// A chat message turn.
    Message {
        /// `user` / `assistant` / `developer` / `system`.
        role: String,
        /// Content blocks.
        #[serde(default)]
        content: Vec<super::ContentBlock>,
        /// Anything else (e.g. `phase`).
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// A built-in function (tool) call.
    FunctionCall {
        /// Tool name.
        name: String,
        /// JSON-encoded argument string.
        #[serde(default)]
        arguments: String,
        /// Provider call id.
        call_id: String,
        /// Anything else.
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// The output of a function call.
    FunctionCallOutput {
        /// The call id this answers.
        call_id: String,
        /// Output (string or structured).
        #[serde(default)]
        output: serde_json::Value,
        /// Anything else.
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// A custom / MCP tool call. **Not yet normalized by the loader.**
    CustomToolCall {
        /// Tool name.
        #[serde(default)]
        name: Option<String>,
        /// JSON-encoded arguments.
        #[serde(default)]
        input: serde_json::Value,
        /// Call id.
        #[serde(default)]
        call_id: Option<String>,
        /// Anything else.
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// A custom / MCP tool result. **Not yet normalized by the loader.**
    CustomToolCallOutput {
        /// The call id this answers.
        #[serde(default)]
        call_id: Option<String>,
        /// Output.
        #[serde(default)]
        output: serde_json::Value,
        /// Anything else.
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// Model reasoning. Not folded 1:1 into a `ChatMessage` (there's no
    /// canonical "reasoning" role), but `summary` text, the raw `content`
    /// chain-of-thought text when genuinely non-null, and a correctly-gated
    /// `encrypted_content` flag (only when that field is non-null — a
    /// present-but-`null` key, which every real rollout carries, must NOT
    /// set the flag) ARE captured by
    /// `crate::session::Session::from_codex_str` onto the next assistant
    /// message's metadata (`reasoning`/`reasoning_content`/
    /// `reasoning_encrypted`), or flushed as their own message when no
    /// following assistant turn exists to attach to — see
    /// `crate::audit::Coverage::Retained` (D5/N1/N2/N3, PARITY-12). The
    /// opaque `encrypted_content` blob itself is not replayed cross-model.
    Reasoning {
        /// Anything (summary, encrypted_content, …).
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// A web-search server tool call. **Not yet normalized.**
    WebSearchCall {
        /// Anything.
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// A tool-search call. **Not yet normalized.**
    ToolSearchCall {
        /// Anything.
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// A tool-search result. **Not yet normalized.**
    ToolSearchOutput {
        /// Anything.
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// An image-generation call. **Not yet normalized.**
    ImageGenerationCall {
        /// Anything.
        #[serde(flatten)]
        extra: ExtraFields,
    },
    /// Any response-item type we do not model yet.
    #[serde(other)]
    Unknown,
}

impl ResponseItem {
    /// The static discriminant name, or `None` for [`ResponseItem::Unknown`].
    pub fn tag(&self) -> Option<&'static str> {
        Some(match self {
            ResponseItem::Message { .. } => "message",
            ResponseItem::FunctionCall { .. } => "function_call",
            ResponseItem::FunctionCallOutput { .. } => "function_call_output",
            ResponseItem::CustomToolCall { .. } => "custom_tool_call",
            ResponseItem::CustomToolCallOutput { .. } => "custom_tool_call_output",
            ResponseItem::Reasoning { .. } => "reasoning",
            ResponseItem::WebSearchCall { .. } => "web_search_call",
            ResponseItem::ToolSearchCall { .. } => "tool_search_call",
            ResponseItem::ToolSearchOutput { .. } => "tool_search_output",
            ResponseItem::ImageGenerationCall { .. } => "image_generation_call",
            ResponseItem::Unknown => return None,
        })
    }

    /// Whether the loader currently normalizes this item into a [`crate::ChatMessage`].
    pub fn is_normalized(&self) -> bool {
        matches!(
            self,
            ResponseItem::Message { .. }
                | ResponseItem::FunctionCall { .. }
                | ResponseItem::FunctionCallOutput { .. }
                | ResponseItem::CustomToolCall { .. }
                | ResponseItem::CustomToolCallOutput { .. }
                | ResponseItem::ToolSearchCall { .. }
                | ResponseItem::ToolSearchOutput { .. }
                | ResponseItem::WebSearchCall { .. }
                | ResponseItem::ImageGenerationCall { .. }
        )
    }
}

/// A UI event echo. We only need its discriminant for the audit.
#[derive(Debug, Clone, Deserialize)]
pub struct EventMsg {
    /// The event subtype (`token_count`, `task_started`, …).
    #[serde(rename = "type", default)]
    pub kind: Option<String>,
    /// Anything else.
    #[serde(flatten)]
    pub extra: ExtraFields,
}