use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiError {
pub code: String,
pub message: String,
}
impl ApiError {
pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
Self {
code: code.into(),
message: message.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeRecord {
pub runtime_id: String,
pub display_name: String,
pub codex_home: Option<String>,
pub codex_binary: String,
pub is_primary: bool,
pub auto_start: bool,
pub created_at_ms: i64,
pub updated_at_ms: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeStatusSnapshot {
pub runtime_id: String,
pub status: String,
pub codex_home: Option<String>,
pub user_agent: Option<String>,
pub platform_family: Option<String>,
pub platform_os: Option<String>,
pub last_error: Option<String>,
pub pid: Option<u32>,
pub updated_at_ms: i64,
}
impl RuntimeStatusSnapshot {
pub fn stopped(runtime_id: impl Into<String>) -> Self {
Self {
runtime_id: runtime_id.into(),
status: "stopped".to_string(),
codex_home: None,
user_agent: None,
platform_family: None,
platform_os: None,
last_error: None,
pid: None,
updated_at_ms: now_millis(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeSummary {
pub runtime_id: String,
pub display_name: String,
pub codex_home: Option<String>,
pub codex_binary: String,
pub is_primary: bool,
pub auto_start: bool,
pub created_at_ms: i64,
pub updated_at_ms: i64,
pub status: RuntimeStatusSnapshot,
}
impl RuntimeSummary {
pub fn from_parts(record: &RuntimeRecord, status: RuntimeStatusSnapshot) -> Self {
Self {
runtime_id: record.runtime_id.clone(),
display_name: record.display_name.clone(),
codex_home: record.codex_home.clone(),
codex_binary: record.codex_binary.clone(),
is_primary: record.is_primary,
auto_start: record.auto_start,
created_at_ms: record.created_at_ms,
updated_at_ms: record.updated_at_ms,
status,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceRecord {
pub id: String,
pub display_name: String,
pub root_path: String,
pub trusted: bool,
pub created_at_ms: i64,
pub updated_at_ms: i64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct ThreadStatusInfo {
pub kind: String,
pub reason: Option<String>,
pub raw: Value,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct ThreadTokenUsage {
pub input_tokens: Option<i64>,
pub cached_input_tokens: Option<i64>,
pub output_tokens: Option<i64>,
pub reasoning_tokens: Option<i64>,
pub total_tokens: Option<i64>,
pub raw: Value,
pub updated_at_ms: i64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct ThreadSummary {
pub id: String,
pub runtime_id: String,
pub workspace_id: Option<String>,
pub name: Option<String>,
pub note: Option<String>,
pub preview: String,
pub cwd: String,
pub status: String,
pub status_info: ThreadStatusInfo,
pub token_usage: Option<ThreadTokenUsage>,
pub model_provider: String,
pub source: String,
pub created_at: i64,
pub updated_at: i64,
pub is_loaded: bool,
pub is_active: bool,
pub archived: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TimelineEntry {
pub id: String,
pub runtime_id: String,
pub thread_id: String,
pub turn_id: Option<String>,
pub item_id: Option<String>,
pub entry_type: String,
pub title: Option<String>,
pub text: String,
pub status: Option<String>,
#[serde(default)]
pub metadata: Value,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct PendingServerRequestOption {
pub label: String,
pub description: Option<String>,
pub value: Option<Value>,
pub is_other: bool,
pub raw: Value,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct PendingServerRequestQuestion {
pub id: String,
pub header: Option<String>,
pub question: Option<String>,
pub required: bool,
pub options: Vec<PendingServerRequestOption>,
pub raw: Value,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct PendingServerRequestRecord {
pub request_id: String,
pub runtime_id: String,
pub rpc_request_id: Value,
pub request_type: String,
pub thread_id: Option<String>,
pub turn_id: Option<String>,
pub item_id: Option<String>,
pub title: Option<String>,
pub reason: Option<String>,
pub command: Option<String>,
pub cwd: Option<String>,
pub grant_root: Option<String>,
pub tool_name: Option<String>,
pub arguments: Option<Value>,
#[serde(default)]
pub questions: Vec<PendingServerRequestQuestion>,
pub proposed_execpolicy_amendment: Option<Value>,
pub network_approval_context: Option<Value>,
pub schema: Option<Value>,
#[serde(default)]
pub available_decisions: Vec<String>,
pub raw_payload: Value,
pub created_at_ms: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PersistedEvent {
pub seq: i64,
pub event_type: String,
pub runtime_id: Option<String>,
pub thread_id: Option<String>,
pub payload: Value,
pub created_at_ms: i64,
}
#[derive(Debug, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ClientEnvelope {
Hello {
device_id: String,
last_ack_seq: Option<i64>,
},
Request {
request_id: String,
action: String,
#[serde(default)]
payload: Value,
},
AckEvents {
last_seq: i64,
},
Ping,
}
#[derive(Debug, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ServerEnvelope {
Hello {
runtime: RuntimeStatusSnapshot,
runtimes: Vec<RuntimeSummary>,
workspaces: Vec<WorkspaceRecord>,
pending_requests: Vec<PendingServerRequestRecord>,
},
Response {
request_id: String,
success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<ApiError>,
},
Event {
seq: i64,
event_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
runtime_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
thread_id: Option<String>,
payload: Value,
},
Pong {
server_time_ms: i64,
},
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetRuntimeStatusRequest {
pub runtime_id: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StartRuntimeRequest {
pub runtime_id: Option<String>,
pub display_name: Option<String>,
pub codex_home: Option<String>,
pub codex_binary: Option<String>,
pub auto_start: Option<bool>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StopRuntimeRequest {
pub runtime_id: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RestartRuntimeRequest {
pub runtime_id: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PruneRuntimeRequest {
pub runtime_id: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpsertWorkspaceRequest {
pub display_name: String,
pub root_path: String,
pub trusted: Option<bool>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListThreadsRequest {
pub workspace_id: Option<String>,
pub runtime_id: Option<String>,
pub limit: Option<usize>,
pub cursor: Option<String>,
pub archived: Option<bool>,
pub search_term: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StartThreadRequest {
pub workspace_id: String,
pub runtime_id: Option<String>,
pub relative_path: Option<String>,
pub model: Option<String>,
pub name: Option<String>,
pub note: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResumeThreadRequest {
pub thread_id: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReadThreadRequest {
pub thread_id: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SendTurnRequest {
pub thread_id: String,
pub text: String,
pub relative_path: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InterruptTurnRequest {
pub thread_id: String,
pub turn_id: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateThreadRequest {
pub thread_id: String,
pub name: Option<String>,
pub note: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArchiveThreadRequest {
pub thread_id: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UnarchiveThreadRequest {
pub thread_id: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RespondPendingRequestRequest {
pub request_id: String,
pub response: Value,
}
pub fn ok_response(request_id: String, data: Value) -> ServerEnvelope {
ServerEnvelope::Response {
request_id,
success: true,
data: Some(data),
error: None,
}
}
pub fn error_response(request_id: String, error: ApiError) -> ServerEnvelope {
ServerEnvelope::Response {
request_id,
success: false,
data: None,
error: Some(error),
}
}
pub fn event_envelope(event: PersistedEvent) -> ServerEnvelope {
ServerEnvelope::Event {
seq: event.seq,
event_type: event.event_type,
runtime_id: event.runtime_id,
thread_id: event.thread_id,
payload: event.payload,
}
}
pub fn now_millis() -> i64 {
let now = std::time::SystemTime::now();
let since_epoch = now
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
since_epoch.as_millis() as i64
}
pub fn json_string(value: &Value) -> String {
match value {
Value::String(inner) => inner.clone(),
_ => value.to_string(),
}
}
pub fn require_payload<T: for<'de> Deserialize<'de>>(payload: Value) -> Result<T> {
Ok(serde_json::from_value(payload)?)
}
pub fn status_payload(runtime: &RuntimeSummary) -> Value {
json!({ "runtime": runtime })
}
pub fn runtime_list_payload(runtimes: &[RuntimeSummary]) -> Value {
json!({ "runtimes": runtimes })
}