Codex Handbook
core/src/responses_metadata.rs 366 lines
use std::collections::BTreeMap;use std::collections::HashMap;use codex_analytics::CompactionImplementation;use codex_analytics::CompactionPhase;use codex_analytics::CompactionReason;use codex_analytics::CompactionStrategy;use codex_analytics::CompactionTrigger;use codex_protocol::ThreadId;use codex_protocol::protocol::InternalSessionSource;use codex_protocol::protocol::SessionSource;use codex_protocol::protocol::SubAgentSource;use codex_utils_string::to_ascii_json_string;use http::HeaderMap as ApiHeaderMap;use http::HeaderValue;use serde::Serialize;use serde_json::Value;use crate::client::X_CODEX_INSTALLATION_ID_HEADER;use crate::client::X_CODEX_PARENT_THREAD_ID_HEADER;use crate::client::X_CODEX_TURN_METADATA_HEADER;use crate::client::X_CODEX_WINDOW_ID_HEADER;use crate::client::X_OPENAI_SUBAGENT_HEADER;pub(crate) const INSTALLATION_ID_KEY: &str = "installation_id";pub(crate) const SESSION_ID_KEY: &str = "session_id";pub(crate) const THREAD_ID_KEY: &str = "thread_id";pub(crate) const TURN_ID_KEY: &str = "turn_id";pub(crate) const WINDOW_ID_KEY: &str = "window_id";pub(crate) const REQUEST_KIND_KEY: &str = "request_kind";pub(crate) const COMPACTION_KEY: &str = "compaction";pub(crate) const TURN_STARTED_AT_UNIX_MS_KEY: &str = "turn_started_at_unix_ms";pub(crate) const FORKED_FROM_THREAD_ID_KEY: &str = "forked_from_thread_id";pub(crate) const PARENT_THREAD_ID_KEY: &str = "parent_thread_id";pub(crate) const SUBAGENT_KIND_KEY: &str = "subagent_kind";pub(crate) const SANDBOX_KEY: &str = "sandbox";pub(crate) const WORKSPACES_KEY: &str = "workspaces";// App-server clients can specify additional metadata in the `responsesapi_client_metadata` param// when submitting a turn, but they must not override fields owned by core.const RESERVED_METADATA_KEYS: &[&str] = &[    INSTALLATION_ID_KEY,    X_CODEX_INSTALLATION_ID_HEADER,    SESSION_ID_KEY,    THREAD_ID_KEY,    TURN_ID_KEY,    WINDOW_ID_KEY,    X_CODEX_WINDOW_ID_HEADER,    X_CODEX_TURN_METADATA_HEADER,    X_CODEX_PARENT_THREAD_ID_HEADER,    X_OPENAI_SUBAGENT_HEADER,    REQUEST_KIND_KEY,    COMPACTION_KEY,    TURN_STARTED_AT_UNIX_MS_KEY,    FORKED_FROM_THREAD_ID_KEY,    PARENT_THREAD_ID_KEY,    SUBAGENT_KIND_KEY,    SANDBOX_KEY,    WORKSPACES_KEY,];/// Metadata attached to model requests whose purpose is conversation compaction.////// This covers both local compaction requests sent through the normal `/responses` path and remote/// compaction requests sent through `/responses/compact`. These fields describe the operation at/// dispatch time. Post-response outcomes such as status, error, duration, and token deltas remain/// in compaction analytics events.#[derive(Clone, Copy, Debug, Serialize)]pub(crate) struct CompactionTurnMetadata {    trigger: CompactionTrigger,    reason: CompactionReason,    implementation: CompactionImplementation,    phase: CompactionPhase,    strategy: CompactionStrategy,}impl CompactionTurnMetadata {    pub(crate) fn new(        trigger: CompactionTrigger,        reason: CompactionReason,        implementation: CompactionImplementation,        phase: CompactionPhase,    ) -> Self {        Self {            trigger,            reason,            implementation,            phase,            strategy: CompactionStrategy::Memento,        }    }}#[derive(Clone, Copy, Debug)]pub(crate) enum CodexResponsesRequestKind {    Turn,    Prewarm,    Compaction(CompactionTurnMetadata),    Memory,}impl CodexResponsesRequestKind {    fn metadata(self) -> (&'static str, Option<CompactionTurnMetadata>) {        match self {            CodexResponsesRequestKind::Turn => ("turn", None),            CodexResponsesRequestKind::Prewarm => ("prewarm", None),            CodexResponsesRequestKind::Compaction(metadata) => ("compaction", Some(metadata)),            CodexResponsesRequestKind::Memory => ("memory", None),        }    }    fn has_turn_identity(self) -> bool {        !matches!(self, CodexResponsesRequestKind::Memory)    }}#[derive(Clone, Debug, Serialize, Default)]pub(crate) struct TurnMetadataWorkspace {    #[serde(default, skip_serializing_if = "Option::is_none")]    pub(crate) associated_remote_urls: Option<BTreeMap<String, String>>,    #[serde(default, skip_serializing_if = "Option::is_none")]    pub(crate) latest_git_commit_hash: Option<String>,    #[serde(default, skip_serializing_if = "Option::is_none")]    pub(crate) has_changes: Option<bool>,}/// Caller-owned snapshot of Codex metadata sent to ResponsesAPI.////// The full Codex turn metadata blob is transported canonically as/// `client_metadata["x-codex-turn-metadata"]`. Flat `client_metadata` keys and direct HTTP/ws/// headers are generated compatibility projections of this snapshot, not separate sources of/// truth.#[derive(Clone, Debug)]pub struct CodexResponsesMetadata {    pub(crate) installation_id: String,    pub(crate) session_id: String,    pub(crate) thread_id: String,    pub(crate) turn_id: Option<String>,    pub(crate) window_id: String,    pub(crate) request_kind: Option<CodexResponsesRequestKind>,    pub(crate) forked_from_thread_id: Option<ThreadId>,    pub(crate) parent_thread_id: Option<ThreadId>,    pub(crate) subagent_header: Option<String>,    pub(crate) subagent_kind: Option<String>,    pub(crate) sandbox: Option<String>,    pub(crate) workspaces: BTreeMap<String, TurnMetadataWorkspace>,    pub(crate) turn_started_at_unix_ms: Option<i64>,    pub(crate) extra: BTreeMap<String, String>,}impl CodexResponsesMetadata {    pub(crate) fn new(        installation_id: String,        session_id: String,        thread_id: String,        window_id: String,    ) -> Self {        Self {            installation_id,            session_id,            thread_id,            turn_id: None,            window_id,            request_kind: None,            forked_from_thread_id: None,            parent_thread_id: None,            subagent_header: None,            subagent_kind: None,            sandbox: None,            workspaces: BTreeMap::new(),            turn_started_at_unix_ms: None,            extra: BTreeMap::new(),        }    }    pub(crate) fn has_turn_metadata(&self) -> bool {        self.request_kind.is_some()    }    pub(crate) fn turn_metadata_json(&self) -> Option<String> {        to_ascii_json_string(&self.turn_metadata_payload()).ok()    }    pub(crate) fn turn_metadata_value(&self) -> Option<Value> {        serde_json::to_value(self.turn_metadata_payload()).ok()    }    pub(crate) fn client_metadata(&self) -> HashMap<String, String> {        let mut client_metadata = HashMap::from([            (                X_CODEX_INSTALLATION_ID_HEADER.to_string(),                self.installation_id.clone(),            ),            (SESSION_ID_KEY.to_string(), self.session_id.clone()),            (THREAD_ID_KEY.to_string(), self.thread_id.clone()),            (X_CODEX_WINDOW_ID_HEADER.to_string(), self.window_id.clone()),        ]);        if let Some(turn_id) = &self.turn_id {            client_metadata.insert(TURN_ID_KEY.to_string(), turn_id.clone());        }        if let Some(subagent_header) = &self.subagent_header {            client_metadata.insert(                X_OPENAI_SUBAGENT_HEADER.to_string(),                subagent_header.clone(),            );        }        if let Some(parent_thread_id) = self.parent_thread_id {            client_metadata.insert(                X_CODEX_PARENT_THREAD_ID_HEADER.to_string(),                parent_thread_id.to_string(),            );        }        if self.has_turn_metadata()            && let Some(turn_metadata_json) = self.turn_metadata_json()        {            client_metadata.insert(X_CODEX_TURN_METADATA_HEADER.to_string(), turn_metadata_json);        }        client_metadata    }    pub(crate) fn compatibility_headers(&self) -> ApiHeaderMap {        let mut headers = ApiHeaderMap::new();        insert_header(&mut headers, X_CODEX_WINDOW_ID_HEADER, &self.window_id);        // Direct x-codex-turn-metadata is compatibility output. New per-request consumers should        // prefer client_metadata["x-codex-turn-metadata"], which is rendered from this same object.        if self.has_turn_metadata()            && let Some(turn_metadata_json) = self.turn_metadata_json()        {            insert_header(                &mut headers,                X_CODEX_TURN_METADATA_HEADER,                &turn_metadata_json,            );        }        if let Some(parent_thread_id) = self.parent_thread_id {            insert_header(                &mut headers,                X_CODEX_PARENT_THREAD_ID_HEADER,                &parent_thread_id.to_string(),            );        }        if let Some(subagent_header) = &self.subagent_header {            insert_header(&mut headers, X_OPENAI_SUBAGENT_HEADER, subagent_header);        }        headers    }    fn turn_metadata_payload(&self) -> CodexTurnMetadataPayload<'_> {        let request_kind = self.request_kind;        let (request_kind_value, compaction) = request_kind.map_or((None, None), |request_kind| {            let (request_kind, compaction) = request_kind.metadata();            (Some(request_kind), compaction)        });        let has_turn_identity =            request_kind.is_none_or(CodexResponsesRequestKind::has_turn_identity);        let has_request_identity =            request_kind.is_some_and(CodexResponsesRequestKind::has_turn_identity);        CodexTurnMetadataPayload {            installation_id: has_request_identity.then_some(self.installation_id.as_str()),            session_id: has_turn_identity.then_some(self.session_id.as_str()),            thread_id: has_turn_identity.then_some(self.thread_id.as_str()),            turn_id: has_turn_identity                .then_some(self.turn_id.as_deref())                .flatten(),            window_id: has_request_identity.then_some(self.window_id.as_str()),            request_kind: request_kind_value,            forked_from_thread_id: self.forked_from_thread_id,            parent_thread_id: self.parent_thread_id,            subagent_kind: self.subagent_kind.as_deref(),            sandbox: self.sandbox.as_deref(),            workspaces: non_empty_workspaces(&self.workspaces),            turn_started_at_unix_ms: self.turn_started_at_unix_ms,            compaction,            // responsesapi_client_metadata enriches the Codex turn metadata blob, not literal            // top-level Responses client_metadata. Reserved Codex-owned keys are filtered when            // these extras enter turn state.            extra: &self.extra,        }    }}pub(crate) fn subagent_header_value(session_source: &SessionSource) -> Option<String> {    match session_source {        SessionSource::SubAgent(subagent_source) => match subagent_source {            SubAgentSource::Review => Some("review".to_string()),            SubAgentSource::Compact => Some("compact".to_string()),            SubAgentSource::MemoryConsolidation => Some("memory_consolidation".to_string()),            SubAgentSource::ThreadSpawn { .. } => Some("collab_spawn".to_string()),            SubAgentSource::Other(label) => Some(label.clone()),        },        SessionSource::Internal(InternalSessionSource::MemoryConsolidation) => {            Some("memory_consolidation".to_string())        }        SessionSource::Cli        | SessionSource::VSCode        | SessionSource::Exec        | SessionSource::Mcp        | SessionSource::Custom(_)        | SessionSource::Unknown => None,    }}pub(crate) fn subagent_metadata_kind(session_source: &SessionSource) -> Option<String> {    match session_source {        SessionSource::SubAgent(subagent_source) => Some(subagent_source.kind().to_string()),        SessionSource::Cli        | SessionSource::VSCode        | SessionSource::Exec        | SessionSource::Mcp        | SessionSource::Custom(_)        | SessionSource::Internal(_)        | SessionSource::Unknown => None,    }}fn insert_header(headers: &mut ApiHeaderMap, name: &'static str, value: &str) {    if let Ok(header_value) = HeaderValue::from_str(value) {        headers.insert(name, header_value);    }}pub(crate) fn filter_extra_metadata(extra: HashMap<String, String>) -> BTreeMap<String, String> {    extra        .into_iter()        .filter(|(key, _)| !RESERVED_METADATA_KEYS.contains(&key.as_str()))        .collect()}fn non_empty_workspaces(    workspaces: &BTreeMap<String, TurnMetadataWorkspace>,) -> Option<&BTreeMap<String, TurnMetadataWorkspace>> {    (!workspaces.is_empty()).then_some(workspaces)}#[derive(Serialize)]struct CodexTurnMetadataPayload<'a> {    #[serde(default, skip_serializing_if = "Option::is_none")]    installation_id: Option<&'a str>,    #[serde(default, skip_serializing_if = "Option::is_none")]    session_id: Option<&'a str>,    #[serde(default, skip_serializing_if = "Option::is_none")]    thread_id: Option<&'a str>,    #[serde(default, skip_serializing_if = "Option::is_none")]    turn_id: Option<&'a str>,    #[serde(default, skip_serializing_if = "Option::is_none")]    window_id: Option<&'a str>,    #[serde(default, skip_serializing_if = "Option::is_none")]    request_kind: Option<&'static str>,    #[serde(default, skip_serializing_if = "Option::is_none")]    forked_from_thread_id: Option<ThreadId>,    #[serde(default, skip_serializing_if = "Option::is_none")]    parent_thread_id: Option<ThreadId>,    #[serde(default, skip_serializing_if = "Option::is_none")]    subagent_kind: Option<&'a str>,    #[serde(default, skip_serializing_if = "Option::is_none")]    sandbox: Option<&'a str>,    #[serde(default, skip_serializing_if = "Option::is_none")]    workspaces: Option<&'a BTreeMap<String, TurnMetadataWorkspace>>,    #[serde(default, skip_serializing_if = "Option::is_none")]    turn_started_at_unix_ms: Option<i64>,    #[serde(default, skip_serializing_if = "Option::is_none")]    compaction: Option<CompactionTurnMetadata>,    #[serde(flatten)]    extra: &'a BTreeMap<String, String>,}