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 bridge_version: String,
252 protocol_version: u32,
253 runtime: RuntimeStatusSnapshot,
254 runtimes: Vec<RuntimeSummary>,
255 workspaces: Vec<WorkspaceRecord>,
256 pending_requests: Vec<PendingServerRequestRecord>,
257 },
258 Response {
259 request_id: String,
260 success: bool,
261 #[serde(skip_serializing_if = "Option::is_none")]
262 data: Option<Value>,
263 #[serde(skip_serializing_if = "Option::is_none")]
264 error: Option<ApiError>,
265 },
266 Event {
267 seq: i64,
268 event_type: String,
269 #[serde(skip_serializing_if = "Option::is_none")]
270 runtime_id: Option<String>,
271 #[serde(skip_serializing_if = "Option::is_none")]
272 thread_id: Option<String>,
273 payload: Value,
274 },
275 Pong {
276 server_time_ms: i64,
277 },
278}
279
280#[derive(Debug, Deserialize)]
281#[serde(rename_all = "camelCase")]
282pub struct GetRuntimeStatusRequest {
283 pub runtime_id: Option<String>,
284}
285
286#[derive(Debug, Deserialize)]
287#[serde(rename_all = "camelCase")]
288pub struct StartRuntimeRequest {
289 pub runtime_id: Option<String>,
290 pub display_name: Option<String>,
291 pub codex_home: Option<String>,
292 pub codex_binary: Option<String>,
293 pub auto_start: Option<bool>,
294}
295
296#[derive(Debug, Deserialize)]
297#[serde(rename_all = "camelCase")]
298pub struct StopRuntimeRequest {
299 pub runtime_id: String,
300}
301
302#[derive(Debug, Deserialize)]
303#[serde(rename_all = "camelCase")]
304pub struct RestartRuntimeRequest {
305 pub runtime_id: String,
306}
307
308#[derive(Debug, Deserialize)]
309#[serde(rename_all = "camelCase")]
310pub struct PruneRuntimeRequest {
311 pub runtime_id: String,
312}
313
314#[derive(Debug, Deserialize)]
315#[serde(rename_all = "camelCase")]
316pub struct UpsertWorkspaceRequest {
317 pub display_name: String,
318 pub root_path: String,
319 pub trusted: Option<bool>,
320}
321
322#[derive(Debug, Deserialize)]
323#[serde(rename_all = "camelCase")]
324pub struct ListThreadsRequest {
325 pub workspace_id: Option<String>,
326 pub runtime_id: Option<String>,
327 pub limit: Option<usize>,
328 pub cursor: Option<String>,
329 pub archived: Option<bool>,
330 pub search_term: Option<String>,
331}
332
333#[derive(Debug, Deserialize)]
334#[serde(rename_all = "camelCase")]
335pub struct StartThreadRequest {
336 pub workspace_id: String,
337 pub runtime_id: Option<String>,
338 pub relative_path: Option<String>,
339 pub model: Option<String>,
340 pub name: Option<String>,
341 pub note: Option<String>,
342}
343
344#[derive(Debug, Deserialize)]
345#[serde(rename_all = "camelCase")]
346pub struct ResumeThreadRequest {
347 pub thread_id: String,
348}
349
350#[derive(Debug, Deserialize)]
351#[serde(rename_all = "camelCase")]
352pub struct ReadThreadRequest {
353 pub thread_id: String,
354}
355
356#[derive(Debug, Deserialize)]
357#[serde(rename_all = "camelCase")]
358pub struct StageInputImageRequest {
359 pub file_name: Option<String>,
360 pub mime_type: Option<String>,
361 pub base64_data: String,
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize)]
365#[serde(rename_all = "camelCase")]
366pub struct StagedInputImage {
367 pub local_path: String,
368 pub display_name: Option<String>,
369 pub mime_type: Option<String>,
370 pub size_bytes: i64,
371}
372
373#[derive(Debug, Clone, Serialize, Deserialize)]
374#[serde(tag = "type", rename_all = "camelCase")]
375pub enum SendTurnInputItem {
376 Text { text: String },
377 LocalImage { path: String },
378}
379
380#[derive(Debug, Deserialize)]
381#[serde(rename_all = "camelCase")]
382pub struct SendTurnRequest {
383 pub thread_id: String,
384 #[serde(default)]
385 pub text: String,
386 pub relative_path: Option<String>,
387 pub input_items: Option<Vec<SendTurnInputItem>>,
388}
389
390#[derive(Debug, Deserialize)]
391#[serde(rename_all = "camelCase")]
392pub struct InterruptTurnRequest {
393 pub thread_id: String,
394 pub turn_id: String,
395}
396
397#[derive(Debug, Deserialize)]
398#[serde(rename_all = "camelCase")]
399pub struct UpdateThreadRequest {
400 pub thread_id: String,
401 pub name: Option<String>,
402 pub note: Option<String>,
403}
404
405#[derive(Debug, Deserialize)]
406#[serde(rename_all = "camelCase")]
407pub struct ArchiveThreadRequest {
408 pub thread_id: String,
409}
410
411#[derive(Debug, Deserialize)]
412#[serde(rename_all = "camelCase")]
413pub struct UnarchiveThreadRequest {
414 pub thread_id: String,
415}
416
417#[derive(Debug, Deserialize)]
418#[serde(rename_all = "camelCase")]
419pub struct RespondPendingRequestRequest {
420 pub request_id: String,
421 pub response: Value,
422}
423
424pub fn ok_response(request_id: String, data: Value) -> ServerEnvelope {
425 ServerEnvelope::Response {
426 request_id,
427 success: true,
428 data: Some(data),
429 error: None,
430 }
431}
432
433pub fn error_response(request_id: String, error: ApiError) -> ServerEnvelope {
434 ServerEnvelope::Response {
435 request_id,
436 success: false,
437 data: None,
438 error: Some(error),
439 }
440}
441
442pub fn event_envelope(event: PersistedEvent) -> ServerEnvelope {
443 ServerEnvelope::Event {
444 seq: event.seq,
445 event_type: event.event_type,
446 runtime_id: event.runtime_id,
447 thread_id: event.thread_id,
448 payload: event.payload,
449 }
450}
451
452pub fn now_millis() -> i64 {
453 let now = std::time::SystemTime::now();
454 let since_epoch = now
455 .duration_since(std::time::UNIX_EPOCH)
456 .unwrap_or_default();
457 since_epoch.as_millis() as i64
458}
459
460pub fn json_string(value: &Value) -> String {
461 match value {
462 Value::String(inner) => inner.clone(),
463 _ => value.to_string(),
464 }
465}
466
467pub fn require_payload<T: for<'de> Deserialize<'de>>(payload: Value) -> Result<T> {
468 Ok(serde_json::from_value(payload)?)
469}
470
471pub fn status_payload(runtime: &RuntimeSummary) -> Value {
472 json!({ "runtime": runtime })
473}
474
475pub fn runtime_list_payload(runtimes: &[RuntimeSummary]) -> Value {
476 json!({ "runtimes": runtimes })
477}