supercode_harness/schema/codex.rs
1//! Typed schema for Codex rollout JSONL (`~/.codex/sessions/.../rollout-*.jsonl`).
2//!
3//! Each line is an envelope `{timestamp, type, payload}`. The `type` selects
4//! the envelope kind; for `response_item` the inner `payload.type` selects the
5//! conversation item. Discriminants we don't model land in `Unknown` variants
6//! so the audit can enumerate them; unmodeled fields land in `extra` maps.
7
8use serde::Deserialize;
9
10use super::ExtraFields;
11
12/// One line of a Codex rollout file.
13#[derive(Debug, Clone, Deserialize)]
14pub struct CodexLine {
15 /// Envelope timestamp.
16 #[serde(default)]
17 pub timestamp: Option<String>,
18 /// The envelope, discriminated by its `type` field.
19 #[serde(flatten)]
20 pub record: CodexRecord,
21}
22
23/// A Codex envelope.
24#[derive(Debug, Clone, Deserialize)]
25#[serde(tag = "type", rename_all = "snake_case")]
26pub enum CodexRecord {
27 /// Session header (id, cwd, base instructions, …).
28 SessionMeta {
29 /// The header payload.
30 payload: SessionMeta,
31 },
32 /// Per-turn context (carries the model, sandbox/approval policy, …).
33 TurnContext {
34 /// The turn-context payload (loosely modeled).
35 payload: TurnContext,
36 },
37 /// A canonical conversation item.
38 ResponseItem {
39 /// The conversation payload, discriminated by its own `type`.
40 payload: ResponseItem,
41 },
42 /// A UI event echo (token counts, task lifecycle, streamed renderings).
43 /// These duplicate `response_item` content and are not part of the
44 /// canonical conversation, but we name the payload type for completeness.
45 EventMsg {
46 /// The event payload.
47 payload: EventMsg,
48 },
49 /// A whole-history compaction record (replaces earlier turns).
50 Compacted {
51 /// The compaction payload.
52 #[serde(default)]
53 payload: serde_json::Value,
54 },
55 /// Any envelope type we do not model yet.
56 #[serde(other)]
57 Unknown,
58}
59
60impl CodexRecord {
61 /// The static discriminant name, or `None` for [`CodexRecord::Unknown`].
62 pub fn tag(&self) -> Option<&'static str> {
63 Some(match self {
64 CodexRecord::SessionMeta { .. } => "session_meta",
65 CodexRecord::TurnContext { .. } => "turn_context",
66 CodexRecord::ResponseItem { .. } => "response_item",
67 CodexRecord::EventMsg { .. } => "event_msg",
68 CodexRecord::Compacted { .. } => "compacted",
69 CodexRecord::Unknown => return None,
70 })
71 }
72}
73
74/// Codex `session_meta` payload.
75#[derive(Debug, Clone, Deserialize)]
76pub struct SessionMeta {
77 /// Session id.
78 #[serde(default)]
79 pub id: Option<String>,
80 /// Working directory.
81 #[serde(default)]
82 pub cwd: Option<String>,
83 /// Base/system instructions (string or `{text}`).
84 #[serde(default)]
85 pub base_instructions: Option<serde_json::Value>,
86 /// Model provider, e.g. `openai`.
87 #[serde(default)]
88 pub model_provider: Option<String>,
89 /// Anything not modeled above.
90 #[serde(flatten)]
91 pub extra: ExtraFields,
92}
93
94/// Codex `turn_context` payload (loosely modeled — many policy fields).
95#[derive(Debug, Clone, Deserialize)]
96pub struct TurnContext {
97 /// The model for this turn.
98 #[serde(default)]
99 pub model: Option<String>,
100 /// Working directory for this turn.
101 #[serde(default)]
102 pub cwd: Option<String>,
103 /// Everything else (approval policy, sandbox policy, effort, …).
104 #[serde(flatten)]
105 pub extra: ExtraFields,
106}
107
108/// A canonical conversation item (`response_item` payload).
109#[derive(Debug, Clone, Deserialize)]
110#[serde(tag = "type", rename_all = "snake_case")]
111pub enum ResponseItem {
112 /// A chat message turn.
113 Message {
114 /// `user` / `assistant` / `developer` / `system`.
115 role: String,
116 /// Content blocks.
117 #[serde(default)]
118 content: Vec<super::ContentBlock>,
119 /// Anything else (e.g. `phase`).
120 #[serde(flatten)]
121 extra: ExtraFields,
122 },
123 /// A built-in function (tool) call.
124 FunctionCall {
125 /// Tool name.
126 name: String,
127 /// JSON-encoded argument string.
128 #[serde(default)]
129 arguments: String,
130 /// Provider call id.
131 call_id: String,
132 /// Anything else.
133 #[serde(flatten)]
134 extra: ExtraFields,
135 },
136 /// The output of a function call.
137 FunctionCallOutput {
138 /// The call id this answers.
139 call_id: String,
140 /// Output (string or structured).
141 #[serde(default)]
142 output: serde_json::Value,
143 /// Anything else.
144 #[serde(flatten)]
145 extra: ExtraFields,
146 },
147 /// A custom / MCP tool call. **Not yet normalized by the loader.**
148 CustomToolCall {
149 /// Tool name.
150 #[serde(default)]
151 name: Option<String>,
152 /// JSON-encoded arguments.
153 #[serde(default)]
154 input: serde_json::Value,
155 /// Call id.
156 #[serde(default)]
157 call_id: Option<String>,
158 /// Anything else.
159 #[serde(flatten)]
160 extra: ExtraFields,
161 },
162 /// A custom / MCP tool result. **Not yet normalized by the loader.**
163 CustomToolCallOutput {
164 /// The call id this answers.
165 #[serde(default)]
166 call_id: Option<String>,
167 /// Output.
168 #[serde(default)]
169 output: serde_json::Value,
170 /// Anything else.
171 #[serde(flatten)]
172 extra: ExtraFields,
173 },
174 /// Model reasoning. Not folded 1:1 into a `ChatMessage` (there's no
175 /// canonical "reasoning" role), but `summary` text, the raw `content`
176 /// chain-of-thought text when genuinely non-null, and a correctly-gated
177 /// `encrypted_content` flag (only when that field is non-null — a
178 /// present-but-`null` key, which every real rollout carries, must NOT
179 /// set the flag) ARE captured by
180 /// `crate::session::Session::from_codex_str` onto the next assistant
181 /// message's metadata (`reasoning`/`reasoning_content`/
182 /// `reasoning_encrypted`), or flushed as their own message when no
183 /// following assistant turn exists to attach to — see
184 /// `crate::audit::Coverage::Retained` (D5/N1/N2/N3, PARITY-12). The
185 /// opaque `encrypted_content` blob itself is not replayed cross-model.
186 Reasoning {
187 /// Anything (summary, encrypted_content, …).
188 #[serde(flatten)]
189 extra: ExtraFields,
190 },
191 /// A web-search server tool call. **Not yet normalized.**
192 WebSearchCall {
193 /// Anything.
194 #[serde(flatten)]
195 extra: ExtraFields,
196 },
197 /// A tool-search call. **Not yet normalized.**
198 ToolSearchCall {
199 /// Anything.
200 #[serde(flatten)]
201 extra: ExtraFields,
202 },
203 /// A tool-search result. **Not yet normalized.**
204 ToolSearchOutput {
205 /// Anything.
206 #[serde(flatten)]
207 extra: ExtraFields,
208 },
209 /// An image-generation call. **Not yet normalized.**
210 ImageGenerationCall {
211 /// Anything.
212 #[serde(flatten)]
213 extra: ExtraFields,
214 },
215 /// Any response-item type we do not model yet.
216 #[serde(other)]
217 Unknown,
218}
219
220impl ResponseItem {
221 /// The static discriminant name, or `None` for [`ResponseItem::Unknown`].
222 pub fn tag(&self) -> Option<&'static str> {
223 Some(match self {
224 ResponseItem::Message { .. } => "message",
225 ResponseItem::FunctionCall { .. } => "function_call",
226 ResponseItem::FunctionCallOutput { .. } => "function_call_output",
227 ResponseItem::CustomToolCall { .. } => "custom_tool_call",
228 ResponseItem::CustomToolCallOutput { .. } => "custom_tool_call_output",
229 ResponseItem::Reasoning { .. } => "reasoning",
230 ResponseItem::WebSearchCall { .. } => "web_search_call",
231 ResponseItem::ToolSearchCall { .. } => "tool_search_call",
232 ResponseItem::ToolSearchOutput { .. } => "tool_search_output",
233 ResponseItem::ImageGenerationCall { .. } => "image_generation_call",
234 ResponseItem::Unknown => return None,
235 })
236 }
237
238 /// Whether the loader currently normalizes this item into a [`crate::ChatMessage`].
239 pub fn is_normalized(&self) -> bool {
240 matches!(
241 self,
242 ResponseItem::Message { .. }
243 | ResponseItem::FunctionCall { .. }
244 | ResponseItem::FunctionCallOutput { .. }
245 | ResponseItem::CustomToolCall { .. }
246 | ResponseItem::CustomToolCallOutput { .. }
247 | ResponseItem::ToolSearchCall { .. }
248 | ResponseItem::ToolSearchOutput { .. }
249 | ResponseItem::WebSearchCall { .. }
250 | ResponseItem::ImageGenerationCall { .. }
251 )
252 }
253}
254
255/// A UI event echo. We only need its discriminant for the audit.
256#[derive(Debug, Clone, Deserialize)]
257pub struct EventMsg {
258 /// The event subtype (`token_count`, `task_started`, …).
259 #[serde(rename = "type", default)]
260 pub kind: Option<String>,
261 /// Anything else.
262 #[serde(flatten)]
263 pub extra: ExtraFields,
264}