use super::CodexErrorInfo; use super::ThreadEnvironment; use super::ThreadItem; use super::ThreadStatus; use super::TurnStatus; use crate::JsonSchema; use crate::TS; use codex_experimental_api_macros::ExperimentalApi; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::MisalignmentErrorDetails as CoreMisalignmentErrorDetails; use codex_protocol::protocol::MisalignmentSteer as CoreMisalignmentSteer; use codex_protocol::protocol::SessionSource as CoreSessionSource; use codex_protocol::protocol::SubAgentSource as CoreSubAgentSource; use codex_protocol::protocol::ThreadHistoryMode as CoreThreadHistoryMode; use codex_protocol::protocol::ThreadSource as CoreThreadSource; use codex_utils_absolute_path::AbsolutePathBuf; #[cfg(test)] use schemars::r#gen::SchemaGenerator; #[cfg(test)] use schemars::schema::Schema; use serde::Deserialize; use serde::Serialize; use std::fmt; use std::path::PathBuf; use thiserror::Error; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase", export_to = "v2/")] #[derive(Default)] pub enum SessionSource { Cli, #[serde(rename = "vscode")] #[ts(rename = "vscode")] #[default] VsCode, Exec, AppServer, Custom(String), SubAgent(CoreSubAgentSource), #[serde(other)] Unknown, } impl From for SessionSource { fn from(value: CoreSessionSource) -> Self { match value { CoreSessionSource::Cli => SessionSource::Cli, CoreSessionSource::VSCode => SessionSource::VsCode, CoreSessionSource::Exec => SessionSource::Exec, CoreSessionSource::Mcp => SessionSource::AppServer, CoreSessionSource::Custom(source) => SessionSource::Custom(source), // We do not want to render those at the app-server level. CoreSessionSource::Internal(_) => SessionSource::Unknown, CoreSessionSource::SubAgent(sub) => SessionSource::SubAgent(sub), CoreSessionSource::Unknown => SessionSource::Unknown, } } } impl From for CoreSessionSource { fn from(value: SessionSource) -> Self { match value { SessionSource::Cli => CoreSessionSource::Cli, SessionSource::VsCode => CoreSessionSource::VSCode, SessionSource::Exec => CoreSessionSource::Exec, SessionSource::AppServer => CoreSessionSource::Mcp, SessionSource::Custom(source) => CoreSessionSource::Custom(source), SessionSource::SubAgent(sub) => CoreSessionSource::SubAgent(sub), SessionSource::Unknown => CoreSessionSource::Unknown, } } } #[derive(Default, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "lowercase")] #[ts(rename_all = "lowercase", export_to = "v2/")] pub enum ThreadHistoryMode { #[default] Legacy, Paginated, } impl From for ThreadHistoryMode { fn from(value: CoreThreadHistoryMode) -> Self { match value { CoreThreadHistoryMode::Legacy => Self::Legacy, CoreThreadHistoryMode::Paginated => Self::Paginated, } } } impl From for CoreThreadHistoryMode { fn from(value: ThreadHistoryMode) -> Self { match value { ThreadHistoryMode::Legacy => Self::Legacy, ThreadHistoryMode::Paginated => Self::Paginated, } } } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, TS)] #[serde(try_from = "String", into = "String")] #[ts(type = "string")] #[ts(export_to = "v2/")] pub enum ThreadSource { User, Subagent, GuardianReview, Feature(String), MemoryConsolidation, } #[cfg(test)] impl JsonSchema for ThreadSource { fn schema_name() -> String { "ThreadSource".to_string() } fn json_schema(generator: &mut SchemaGenerator) -> Schema { String::json_schema(generator) } } impl TryFrom for ThreadSource { type Error = String; fn try_from(value: String) -> Result { value.parse::().map(Into::into) } } impl From for String { fn from(value: ThreadSource) -> Self { CoreThreadSource::from(value).into() } } impl From for ThreadSource { fn from(value: CoreThreadSource) -> Self { match value { CoreThreadSource::User => ThreadSource::User, CoreThreadSource::Subagent => ThreadSource::Subagent, CoreThreadSource::GuardianReview => ThreadSource::GuardianReview, CoreThreadSource::Feature(feature) => ThreadSource::Feature(feature), CoreThreadSource::MemoryConsolidation => ThreadSource::MemoryConsolidation, } } } impl From for CoreThreadSource { fn from(value: ThreadSource) -> Self { match value { ThreadSource::User => CoreThreadSource::User, ThreadSource::Subagent => CoreThreadSource::Subagent, ThreadSource::GuardianReview => CoreThreadSource::GuardianReview, ThreadSource::Feature(feature) => CoreThreadSource::Feature(feature), ThreadSource::MemoryConsolidation => CoreThreadSource::MemoryConsolidation, } } } /// Extra app-server data for a thread. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase", export_to = "v2/")] pub struct ThreadExtra {} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct GitInfo { pub sha: Option, pub branch: Option, pub origin_url: Option, } /// An independently persisted, user-visible thread section. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct ThreadSection { /// Opaque UUIDv7 identity that remains stable when the section is renamed. pub id: String, /// The current user-visible section name. pub name: String, /// Optional appearance synchronized across clients. #[serde(default)] pub appearance: Option, } /// Extensible visual presentation for a custom thread section. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct ThreadSectionAppearance { pub icon: Option, pub color: Option, } #[derive(Serialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct Thread { /// Identifier for this thread. Codex-generated thread IDs are UUIDv7. pub id: String, /// Current environments for a loaded thread, in priority order, primary first. /// `null` means the thread is not loaded or the server does not expose its selection. /// An empty list means no environments are selected. This does not report connection status. #[experimental("thread.environments")] #[serde(default)] pub environments: Option>, /// Optional implementation-specific thread data. #[experimental("thread.extra")] pub extra: Option, /// Session id shared by threads that belong to the same session tree. pub session_id: String, /// Source thread id when this thread was created by forking another thread. pub forked_from_id: Option, /// The ID of the parent thread. This will only be set if this thread is a subagent. pub parent_thread_id: Option, /// Usually the first user message in the thread, if available. pub preview: String, /// Whether the thread is ephemeral and should not be materialized on disk. pub ephemeral: bool, /// The independently persisted section selected for this thread, if any. #[serde(default)] pub section: Option, /// Unix timestamp in seconds when the thread entered its current section. #[serde(default)] #[ts(type = "number | null")] pub section_entered_at: Option, /// Canonical project assignment owned by app-server, if any. #[schemars( required, schema_with = "crate::protocol::serde_helpers::nullable_string_schema" )] pub project_id: Option, /// Persisted thread history contract selected when this thread was created. #[serde(default)] pub history_mode: ThreadHistoryMode, /// Model provider used for this thread (for example, 'openai'). pub model_provider: String, /// Current configured model when loaded, otherwise the latest persisted model. /// Null when unavailable. This is not per-turn execution telemetry. pub model: Option, /// Current configured reasoning effort when loaded, otherwise the latest persisted effort. /// Null when unset or unavailable. This is not per-turn execution telemetry. pub reasoning_effort: Option, /// Unix timestamp (in seconds) when the thread was created. #[ts(type = "number")] pub created_at: i64, /// Unix timestamp (in seconds) when the thread was last updated. #[ts(type = "number")] pub updated_at: i64, /// Unix timestamp (in seconds) used for thread recency ordering. #[ts(type = "number | null")] pub recency_at: Option, /// Current runtime status for the thread. pub status: ThreadStatus, /// [UNSTABLE] Path to the thread on disk. pub path: Option, /// Working directory captured for the thread. pub cwd: AbsolutePathBuf, /// Version of the CLI that created the thread. pub cli_version: String, /// Originator recorded when the thread was created, independent of its current client or executor. /// Null when the recorded originator is unavailable. pub originator: Option, /// Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.). pub source: SessionSource, /// Whether the app server accepts direct turn input for this loaded thread. /// `None` means the capability is unavailable, such as for an unloaded stored thread. #[experimental("thread.canAcceptDirectInput")] pub can_accept_direct_input: Option, /// Optional analytics source classification for this thread. pub thread_source: Option, /// Optional random unique nickname assigned to an AgentControl-spawned sub-agent. pub agent_nickname: Option, /// Optional role (agent_role) assigned to an AgentControl-spawned sub-agent. pub agent_role: Option, /// Optional Git metadata captured when the thread was created. pub git_info: Option, /// Optional user-facing thread title. pub name: Option, /// Saved Daybreak choice, independent of turn execution. Null if unset. #[experimental("thread.daybreakEnabled")] pub daybreak_enabled: Option, /// Only populated on `thread/resume`, `thread/fork`, and `thread/read` /// (when `includeTurns` is true) responses. /// For all other responses and notifications returning a Thread, /// the turns field will be an empty list. pub turns: Vec, } // TODO: Remove this compatibility decoder after app-server versions that omitted // `projectId` have aged out of the supported TUI -> remote app-server version-skew window. #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct ThreadCompatibility { id: String, #[serde(default)] environments: Option>, extra: Option, session_id: String, forked_from_id: Option, parent_thread_id: Option, preview: String, ephemeral: bool, #[serde(default)] section: Option, #[serde(default)] section_entered_at: Option, #[serde(default)] project_id: Option, #[serde(default)] history_mode: ThreadHistoryMode, model_provider: String, model: Option, reasoning_effort: Option, created_at: i64, updated_at: i64, recency_at: Option, status: ThreadStatus, path: Option, cwd: AbsolutePathBuf, cli_version: String, originator: Option, source: SessionSource, can_accept_direct_input: Option, thread_source: Option, agent_nickname: Option, agent_role: Option, git_info: Option, name: Option, daybreak_enabled: Option, turns: Vec, } impl<'de> Deserialize<'de> for Thread { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let thread = ThreadCompatibility::deserialize(deserializer)?; Ok(Self { id: thread.id, environments: thread.environments, extra: thread.extra, session_id: thread.session_id, forked_from_id: thread.forked_from_id, parent_thread_id: thread.parent_thread_id, preview: thread.preview, ephemeral: thread.ephemeral, section: thread.section, section_entered_at: thread.section_entered_at, project_id: thread.project_id, history_mode: thread.history_mode, model_provider: thread.model_provider, model: thread.model, reasoning_effort: thread.reasoning_effort, created_at: thread.created_at, updated_at: thread.updated_at, recency_at: thread.recency_at, status: thread.status, path: thread.path, cwd: thread.cwd, cli_version: thread.cli_version, originator: thread.originator, source: thread.source, can_accept_direct_input: thread.can_accept_direct_input, thread_source: thread.thread_source, agent_nickname: thread.agent_nickname, agent_role: thread.agent_role, git_info: thread.git_info, name: thread.name, daybreak_enabled: thread.daybreak_enabled, turns: thread.turns, }) } } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct Turn { /// Identifier for this turn. Codex-generated turn IDs are UUIDv7. pub id: String, /// Thread items currently included in this turn payload. pub items: Vec, /// Describes how much of `items` has been loaded for this turn. #[serde(default)] pub items_view: TurnItemsView, pub status: TurnStatus, /// Only populated when the Turn's status is failed. pub error: Option, /// Unix timestamp (in seconds) when the turn started. #[ts(type = "number | null")] pub started_at: Option, /// Unix timestamp (in seconds) when the turn completed. #[ts(type = "number | null")] pub completed_at: Option, /// Duration between turn start and completion in milliseconds, if known. #[ts(type = "number | null")] pub duration_ms: Option, } #[derive(Default, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub enum TurnItemsView { /// `items` was not loaded for this turn. The field is intentionally empty. NotLoaded, /// `items` contains only a display summary for this turn. Summary, /// `items` contains every ThreadItem available from persisted app-server history for this turn. #[default] Full, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, Error)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] #[error("{message}")] pub struct TurnError { pub message: String, pub codex_error_info: Option, #[serde(default)] pub additional_details: Option, /// Optional public explanation and continuation instruction for a misalignment block. #[serde(default)] pub misalignment: Option, } #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct MisalignmentErrorDetails { /// Open-ended classification; clients must accept categories added by Responses. pub error_type: Option, /// A substantive localized explanation is required before offering continuation. pub detailed_explanation: Option, /// Instruction to submit as the next turn's user input if continuation is confirmed. pub steer: Option, } impl fmt::Debug for MisalignmentErrorDetails { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("MisalignmentErrorDetails") .field("error_type", &self.error_type) .field( "has_detailed_explanation", &self.detailed_explanation.is_some(), ) .field("has_steer", &self.steer.is_some()) .finish() } } impl From for MisalignmentErrorDetails { fn from(value: CoreMisalignmentErrorDetails) -> Self { Self { error_type: value.error_type, detailed_explanation: value.detailed_explanation, steer: value.steer.map(Into::into), } } } #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct MisalignmentSteer { pub message: String, } impl fmt::Debug for MisalignmentSteer { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("MisalignmentSteer") .field("message", &"[REDACTED]") .finish() } } impl From for MisalignmentSteer { fn from(value: CoreMisalignmentSteer) -> Self { Self { message: value.message, } } }