Skip to main content

codex_mobile_bridge/
bridge_protocol.rs

1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use serde_json::{Value, json};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct ApiError {
7    pub code: String,
8    pub message: String,
9}
10
11impl ApiError {
12    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
13        Self {
14            code: code.into(),
15            message: message.into(),
16        }
17    }
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub struct RuntimeRecord {
23    pub runtime_id: String,
24    pub display_name: String,
25    pub codex_home: Option<String>,
26    pub codex_binary: String,
27    pub is_primary: bool,
28    pub auto_start: bool,
29    pub created_at_ms: i64,
30    pub updated_at_ms: i64,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34#[serde(rename_all = "camelCase")]
35pub struct RuntimeStatusSnapshot {
36    pub runtime_id: String,
37    pub status: String,
38    pub codex_home: Option<String>,
39    pub user_agent: Option<String>,
40    pub platform_family: Option<String>,
41    pub platform_os: Option<String>,
42    pub last_error: Option<String>,
43    pub pid: Option<u32>,
44    pub updated_at_ms: i64,
45}
46
47impl RuntimeStatusSnapshot {
48    pub fn stopped(runtime_id: impl Into<String>) -> Self {
49        Self {
50            runtime_id: runtime_id.into(),
51            status: "stopped".to_string(),
52            codex_home: None,
53            user_agent: None,
54            platform_family: None,
55            platform_os: None,
56            last_error: None,
57            pid: None,
58            updated_at_ms: now_millis(),
59        }
60    }
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub struct RuntimeSummary {
66    pub runtime_id: String,
67    pub display_name: String,
68    pub codex_home: Option<String>,
69    pub codex_binary: String,
70    pub is_primary: bool,
71    pub auto_start: bool,
72    pub created_at_ms: i64,
73    pub updated_at_ms: i64,
74    pub status: RuntimeStatusSnapshot,
75}
76
77impl RuntimeSummary {
78    pub fn from_parts(record: &RuntimeRecord, status: RuntimeStatusSnapshot) -> Self {
79        Self {
80            runtime_id: record.runtime_id.clone(),
81            display_name: record.display_name.clone(),
82            codex_home: record.codex_home.clone(),
83            codex_binary: record.codex_binary.clone(),
84            is_primary: record.is_primary,
85            auto_start: record.auto_start,
86            created_at_ms: record.created_at_ms,
87            updated_at_ms: record.updated_at_ms,
88            status,
89        }
90    }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94#[serde(rename_all = "camelCase")]
95pub struct WorkspaceRecord {
96    pub id: String,
97    pub display_name: String,
98    pub root_path: String,
99    pub trusted: bool,
100    pub created_at_ms: i64,
101    pub updated_at_ms: i64,
102}
103
104#[derive(Debug, Clone, Default, Serialize, Deserialize)]
105#[serde(rename_all = "camelCase")]
106#[serde(default)]
107pub struct ThreadStatusInfo {
108    pub kind: String,
109    pub reason: Option<String>,
110    pub raw: Value,
111}
112
113#[derive(Debug, Clone, Default, Serialize, Deserialize)]
114#[serde(rename_all = "camelCase")]
115#[serde(default)]
116pub struct ThreadTokenUsage {
117    pub input_tokens: Option<i64>,
118    pub cached_input_tokens: Option<i64>,
119    pub output_tokens: Option<i64>,
120    pub reasoning_tokens: Option<i64>,
121    pub total_tokens: Option<i64>,
122    pub raw: Value,
123    pub updated_at_ms: i64,
124}
125
126#[derive(Debug, Clone, Default, Serialize, Deserialize)]
127#[serde(rename_all = "camelCase")]
128#[serde(default)]
129pub struct ThreadSummary {
130    pub id: String,
131    pub runtime_id: String,
132    pub workspace_id: Option<String>,
133    pub name: Option<String>,
134    pub note: Option<String>,
135    pub preview: String,
136    pub cwd: String,
137    pub status: String,
138    pub status_info: ThreadStatusInfo,
139    pub token_usage: Option<ThreadTokenUsage>,
140    pub model_provider: String,
141    pub source: String,
142    pub created_at: i64,
143    pub updated_at: i64,
144    pub is_loaded: bool,
145    pub is_active: bool,
146    pub archived: bool,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
150#[serde(rename_all = "camelCase")]
151pub struct TimelineEntry {
152    pub id: String,
153    pub runtime_id: String,
154    pub thread_id: String,
155    pub turn_id: Option<String>,
156    pub item_id: Option<String>,
157    pub entry_type: String,
158    pub title: Option<String>,
159    pub text: String,
160    pub status: Option<String>,
161    #[serde(default)]
162    pub metadata: Value,
163}
164
165#[derive(Debug, Clone, Default, Serialize, Deserialize)]
166#[serde(rename_all = "camelCase")]
167#[serde(default)]
168pub struct PendingServerRequestOption {
169    pub label: String,
170    pub description: Option<String>,
171    pub value: Option<Value>,
172    pub is_other: bool,
173    pub raw: Value,
174}
175
176#[derive(Debug, Clone, Default, Serialize, Deserialize)]
177#[serde(rename_all = "camelCase")]
178#[serde(default)]
179pub struct PendingServerRequestQuestion {
180    pub id: String,
181    pub header: Option<String>,
182    pub question: Option<String>,
183    pub required: bool,
184    pub options: Vec<PendingServerRequestOption>,
185    pub raw: Value,
186}
187
188#[derive(Debug, Clone, Default, Serialize, Deserialize)]
189#[serde(rename_all = "camelCase")]
190#[serde(default)]
191pub struct PendingServerRequestRecord {
192    pub request_id: String,
193    pub runtime_id: String,
194    pub rpc_request_id: Value,
195    pub request_type: String,
196    pub thread_id: Option<String>,
197    pub turn_id: Option<String>,
198    pub item_id: Option<String>,
199    pub title: Option<String>,
200    pub reason: Option<String>,
201    pub command: Option<String>,
202    pub cwd: Option<String>,
203    pub grant_root: Option<String>,
204    pub tool_name: Option<String>,
205    pub arguments: Option<Value>,
206    #[serde(default)]
207    pub questions: Vec<PendingServerRequestQuestion>,
208    pub proposed_execpolicy_amendment: Option<Value>,
209    pub network_approval_context: Option<Value>,
210    pub schema: Option<Value>,
211    #[serde(default)]
212    pub available_decisions: Vec<String>,
213    pub raw_payload: Value,
214    pub created_at_ms: i64,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
218#[serde(rename_all = "camelCase")]
219pub struct PersistedEvent {
220    pub seq: i64,
221    pub event_type: String,
222    pub runtime_id: Option<String>,
223    pub thread_id: Option<String>,
224    pub payload: Value,
225    pub created_at_ms: i64,
226}
227
228#[derive(Debug, Deserialize)]
229#[serde(tag = "kind", rename_all = "snake_case")]
230pub enum ClientEnvelope {
231    Hello {
232        device_id: String,
233        last_ack_seq: Option<i64>,
234    },
235    Request {
236        request_id: String,
237        action: String,
238        #[serde(default)]
239        payload: Value,
240    },
241    AckEvents {
242        last_seq: i64,
243    },
244    Ping,
245}
246
247#[derive(Debug, Serialize)]
248#[serde(tag = "kind", rename_all = "snake_case")]
249pub enum ServerEnvelope {
250    Hello {
251        runtime: RuntimeStatusSnapshot,
252        runtimes: Vec<RuntimeSummary>,
253        workspaces: Vec<WorkspaceRecord>,
254        pending_requests: Vec<PendingServerRequestRecord>,
255    },
256    Response {
257        request_id: String,
258        success: bool,
259        #[serde(skip_serializing_if = "Option::is_none")]
260        data: Option<Value>,
261        #[serde(skip_serializing_if = "Option::is_none")]
262        error: Option<ApiError>,
263    },
264    Event {
265        seq: i64,
266        event_type: String,
267        #[serde(skip_serializing_if = "Option::is_none")]
268        runtime_id: Option<String>,
269        #[serde(skip_serializing_if = "Option::is_none")]
270        thread_id: Option<String>,
271        payload: Value,
272    },
273    Pong {
274        server_time_ms: i64,
275    },
276}
277
278#[derive(Debug, Deserialize)]
279#[serde(rename_all = "camelCase")]
280pub struct GetRuntimeStatusRequest {
281    pub runtime_id: Option<String>,
282}
283
284#[derive(Debug, Deserialize)]
285#[serde(rename_all = "camelCase")]
286pub struct StartRuntimeRequest {
287    pub runtime_id: Option<String>,
288    pub display_name: Option<String>,
289    pub codex_home: Option<String>,
290    pub codex_binary: Option<String>,
291    pub auto_start: Option<bool>,
292}
293
294#[derive(Debug, Deserialize)]
295#[serde(rename_all = "camelCase")]
296pub struct StopRuntimeRequest {
297    pub runtime_id: String,
298}
299
300#[derive(Debug, Deserialize)]
301#[serde(rename_all = "camelCase")]
302pub struct RestartRuntimeRequest {
303    pub runtime_id: String,
304}
305
306#[derive(Debug, Deserialize)]
307#[serde(rename_all = "camelCase")]
308pub struct PruneRuntimeRequest {
309    pub runtime_id: String,
310}
311
312#[derive(Debug, Deserialize)]
313#[serde(rename_all = "camelCase")]
314pub struct UpsertWorkspaceRequest {
315    pub display_name: String,
316    pub root_path: String,
317    pub trusted: Option<bool>,
318}
319
320#[derive(Debug, Deserialize)]
321#[serde(rename_all = "camelCase")]
322pub struct ListThreadsRequest {
323    pub workspace_id: Option<String>,
324    pub runtime_id: Option<String>,
325    pub limit: Option<usize>,
326    pub cursor: Option<String>,
327    pub archived: Option<bool>,
328    pub search_term: Option<String>,
329}
330
331#[derive(Debug, Deserialize)]
332#[serde(rename_all = "camelCase")]
333pub struct StartThreadRequest {
334    pub workspace_id: String,
335    pub runtime_id: Option<String>,
336    pub relative_path: Option<String>,
337    pub model: Option<String>,
338    pub name: Option<String>,
339    pub note: Option<String>,
340}
341
342#[derive(Debug, Deserialize)]
343#[serde(rename_all = "camelCase")]
344pub struct ResumeThreadRequest {
345    pub thread_id: String,
346}
347
348#[derive(Debug, Deserialize)]
349#[serde(rename_all = "camelCase")]
350pub struct ReadThreadRequest {
351    pub thread_id: String,
352}
353
354#[derive(Debug, Deserialize)]
355#[serde(rename_all = "camelCase")]
356pub struct SendTurnRequest {
357    pub thread_id: String,
358    pub text: String,
359    pub relative_path: Option<String>,
360}
361
362#[derive(Debug, Deserialize)]
363#[serde(rename_all = "camelCase")]
364pub struct InterruptTurnRequest {
365    pub thread_id: String,
366    pub turn_id: String,
367}
368
369#[derive(Debug, Deserialize)]
370#[serde(rename_all = "camelCase")]
371pub struct UpdateThreadRequest {
372    pub thread_id: String,
373    pub name: Option<String>,
374    pub note: Option<String>,
375}
376
377#[derive(Debug, Deserialize)]
378#[serde(rename_all = "camelCase")]
379pub struct ArchiveThreadRequest {
380    pub thread_id: String,
381}
382
383#[derive(Debug, Deserialize)]
384#[serde(rename_all = "camelCase")]
385pub struct UnarchiveThreadRequest {
386    pub thread_id: String,
387}
388
389#[derive(Debug, Deserialize)]
390#[serde(rename_all = "camelCase")]
391pub struct RespondPendingRequestRequest {
392    pub request_id: String,
393    pub response: Value,
394}
395
396pub fn ok_response(request_id: String, data: Value) -> ServerEnvelope {
397    ServerEnvelope::Response {
398        request_id,
399        success: true,
400        data: Some(data),
401        error: None,
402    }
403}
404
405pub fn error_response(request_id: String, error: ApiError) -> ServerEnvelope {
406    ServerEnvelope::Response {
407        request_id,
408        success: false,
409        data: None,
410        error: Some(error),
411    }
412}
413
414pub fn event_envelope(event: PersistedEvent) -> ServerEnvelope {
415    ServerEnvelope::Event {
416        seq: event.seq,
417        event_type: event.event_type,
418        runtime_id: event.runtime_id,
419        thread_id: event.thread_id,
420        payload: event.payload,
421    }
422}
423
424pub fn now_millis() -> i64 {
425    let now = std::time::SystemTime::now();
426    let since_epoch = now
427        .duration_since(std::time::UNIX_EPOCH)
428        .unwrap_or_default();
429    since_epoch.as_millis() as i64
430}
431
432pub fn json_string(value: &Value) -> String {
433    match value {
434        Value::String(inner) => inner.clone(),
435        _ => value.to_string(),
436    }
437}
438
439pub fn require_payload<T: for<'de> Deserialize<'de>>(payload: Value) -> Result<T> {
440    Ok(serde_json::from_value(payload)?)
441}
442
443pub fn status_payload(runtime: &RuntimeSummary) -> Value {
444    json!({ "runtime": runtime })
445}
446
447pub fn runtime_list_payload(runtimes: &[RuntimeSummary]) -> Value {
448    json!({ "runtimes": runtimes })
449}