1use std::collections::HashMap;
2use std::fmt::Display;
3use std::path::PathBuf;
4
5use crate::config_types::ReasoningEffort;
6use crate::config_types::ReasoningSummary;
7use crate::config_types::SandboxMode;
8use crate::protocol::AskForApproval;
9use crate::protocol::FileChange;
10use crate::protocol::ReviewDecision;
11use crate::protocol::SandboxPolicy;
12use crate::protocol::TurnAbortReason;
13use agcodex_mcp_types::RequestId;
14use serde::Deserialize;
15use serde::Serialize;
16use ts_rs::TS;
17use uuid::Uuid;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
20#[ts(type = "string")]
21pub struct ConversationId(pub Uuid);
22
23impl Display for ConversationId {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 write!(f, "{}", self.0)
26 }
27}
28
29#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, TS)]
30#[ts(type = "string")]
31pub struct GitSha(pub String);
32
33impl GitSha {
34 pub fn new(sha: &str) -> Self {
35 Self(sha.to_string())
36 }
37}
38
39#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
41#[serde(tag = "method", rename_all = "camelCase")]
42pub enum ClientRequest {
43 NewConversation {
44 #[serde(rename = "id")]
45 request_id: RequestId,
46 params: NewConversationParams,
47 },
48 SendUserMessage {
49 #[serde(rename = "id")]
50 request_id: RequestId,
51 params: SendUserMessageParams,
52 },
53 SendUserTurn {
54 #[serde(rename = "id")]
55 request_id: RequestId,
56 params: SendUserTurnParams,
57 },
58 InterruptConversation {
59 #[serde(rename = "id")]
60 request_id: RequestId,
61 params: InterruptConversationParams,
62 },
63 AddConversationListener {
64 #[serde(rename = "id")]
65 request_id: RequestId,
66 params: AddConversationListenerParams,
67 },
68 RemoveConversationListener {
69 #[serde(rename = "id")]
70 request_id: RequestId,
71 params: RemoveConversationListenerParams,
72 },
73 LoginChatGpt {
74 #[serde(rename = "id")]
75 request_id: RequestId,
76 },
77 CancelLoginChatGpt {
78 #[serde(rename = "id")]
79 request_id: RequestId,
80 params: CancelLoginChatGptParams,
81 },
82 GitDiffToRemote {
83 #[serde(rename = "id")]
84 request_id: RequestId,
85 params: GitDiffToRemoteParams,
86 },
87}
88
89#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, TS)]
90#[serde(rename_all = "camelCase")]
91pub struct NewConversationParams {
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub model: Option<String>,
95
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub profile: Option<String>,
99
100 #[serde(skip_serializing_if = "Option::is_none")]
103 pub cwd: Option<String>,
104
105 #[serde(skip_serializing_if = "Option::is_none")]
108 pub approval_policy: Option<AskForApproval>,
109
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub sandbox: Option<SandboxMode>,
113
114 #[serde(skip_serializing_if = "Option::is_none")]
117 pub config: Option<HashMap<String, serde_json::Value>>,
118
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub base_instructions: Option<String>,
122
123 #[serde(skip_serializing_if = "Option::is_none")]
125 pub include_plan_tool: Option<bool>,
126
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub include_apply_patch_tool: Option<bool>,
130}
131
132#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
133#[serde(rename_all = "camelCase")]
134pub struct NewConversationResponse {
135 pub conversation_id: ConversationId,
136 pub model: String,
137}
138
139#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
140#[serde(rename_all = "camelCase")]
141pub struct AddConversationSubscriptionResponse {
142 pub subscription_id: Uuid,
143}
144
145#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
146#[serde(rename_all = "camelCase")]
147pub struct RemoveConversationSubscriptionResponse {}
148
149#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
150#[serde(rename_all = "camelCase")]
151pub struct LoginChatGptResponse {
152 pub login_id: Uuid,
153 pub auth_url: String,
155}
156
157#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
158#[serde(rename_all = "camelCase")]
159pub struct GitDiffToRemoteResponse {
160 pub sha: GitSha,
161 pub diff: String,
162}
163
164pub const LOGIN_CHATGPT_COMPLETE_EVENT: &str = "codex/event/login_chatgpt_complete";
166
167#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
168#[serde(rename_all = "camelCase")]
169pub struct LoginChatGptCompleteNotification {
170 pub login_id: Uuid,
171 pub success: bool,
172 #[serde(skip_serializing_if = "Option::is_none")]
173 pub error: Option<String>,
174}
175
176#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
177#[serde(rename_all = "camelCase")]
178pub struct CancelLoginChatGptParams {
179 pub login_id: Uuid,
180}
181
182#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
183#[serde(rename_all = "camelCase")]
184pub struct GitDiffToRemoteParams {
185 pub cwd: PathBuf,
186}
187
188#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
189#[serde(rename_all = "camelCase")]
190pub struct CancelLoginChatGptResponse {}
191
192#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
193#[serde(rename_all = "camelCase")]
194pub struct SendUserMessageParams {
195 pub conversation_id: ConversationId,
196 pub items: Vec<InputItem>,
197}
198
199#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
200#[serde(rename_all = "camelCase")]
201pub struct SendUserTurnParams {
202 pub conversation_id: ConversationId,
203 pub items: Vec<InputItem>,
204 pub cwd: PathBuf,
205 pub approval_policy: AskForApproval,
206 pub sandbox_policy: SandboxPolicy,
207 pub model: String,
208 pub effort: ReasoningEffort,
209 pub summary: ReasoningSummary,
210}
211
212#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
213#[serde(rename_all = "camelCase")]
214pub struct SendUserTurnResponse {}
215
216#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
217#[serde(rename_all = "camelCase")]
218pub struct InterruptConversationParams {
219 pub conversation_id: ConversationId,
220}
221
222#[derive(Serialize, Deserialize, Debug, Clone, TS)]
223#[serde(rename_all = "camelCase")]
224pub struct InterruptConversationResponse {
225 pub abort_reason: TurnAbortReason,
226}
227
228#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
229#[serde(rename_all = "camelCase")]
230pub struct SendUserMessageResponse {}
231
232#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
233#[serde(rename_all = "camelCase")]
234pub struct AddConversationListenerParams {
235 pub conversation_id: ConversationId,
236}
237
238#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
239#[serde(rename_all = "camelCase")]
240pub struct RemoveConversationListenerParams {
241 pub subscription_id: Uuid,
242}
243
244#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
245#[serde(rename_all = "camelCase")]
246#[serde(tag = "type", content = "data")]
247pub enum InputItem {
248 Text {
249 text: String,
250 },
251 Image {
253 image_url: String,
254 },
255
256 LocalImage {
259 path: PathBuf,
260 },
261}
262
263pub const APPLY_PATCH_APPROVAL_METHOD: &str = "applyPatchApproval";
266pub const EXEC_COMMAND_APPROVAL_METHOD: &str = "execCommandApproval";
267
268#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
270#[serde(tag = "method", rename_all = "camelCase")]
271pub enum ServerRequest {
272 ApplyPatchApproval {
274 #[serde(rename = "id")]
275 request_id: RequestId,
276 params: ApplyPatchApprovalParams,
277 },
278 ExecCommandApproval {
280 #[serde(rename = "id")]
281 request_id: RequestId,
282 params: ExecCommandApprovalParams,
283 },
284}
285
286#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
287pub struct ApplyPatchApprovalParams {
288 pub conversation_id: ConversationId,
289 pub call_id: String,
292 pub file_changes: HashMap<PathBuf, FileChange>,
293 #[serde(skip_serializing_if = "Option::is_none")]
295 pub reason: Option<String>,
296 #[serde(skip_serializing_if = "Option::is_none")]
299 pub grant_root: Option<PathBuf>,
300}
301
302#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
303pub struct ExecCommandApprovalParams {
304 pub conversation_id: ConversationId,
305 pub call_id: String,
308 pub command: Vec<String>,
309 pub cwd: PathBuf,
310 #[serde(skip_serializing_if = "Option::is_none")]
311 pub reason: Option<String>,
312}
313
314#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
315pub struct ExecCommandApprovalResponse {
316 pub decision: ReviewDecision,
317}
318
319#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
320pub struct ApplyPatchApprovalResponse {
321 pub decision: ReviewDecision,
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327 use pretty_assertions::assert_eq;
328 use serde_json::json;
329
330 #[test]
331 fn serialize_new_conversation() {
332 let request = ClientRequest::NewConversation {
333 request_id: RequestId::Integer(42),
334 params: NewConversationParams {
335 model: Some("gpt-5".to_string()),
336 profile: None,
337 cwd: None,
338 approval_policy: Some(AskForApproval::OnRequest),
339 sandbox: None,
340 config: None,
341 base_instructions: None,
342 include_plan_tool: None,
343 include_apply_patch_tool: None,
344 },
345 };
346 assert_eq!(
347 json!({
348 "method": "newConversation",
349 "id": 42,
350 "params": {
351 "model": "gpt-5",
352 "approvalPolicy": "on-request"
353 }
354 }),
355 serde_json::to_value(&request).unwrap(),
356 );
357 }
358}