mirror of
https://github.com/openai/codex.git
synced 2026-04-29 02:41:12 +03:00
2244 lines
73 KiB
Markdown
2244 lines
73 KiB
Markdown
# PR #1715: Mcp protocol
|
|
|
|
- URL: https://github.com/openai/codex/pull/1715
|
|
- Author: aibrahim-oai
|
|
- Created: 2025-07-28 22:28:09 UTC
|
|
- Updated: 2025-07-30 03:14:54 UTC
|
|
- Changes: +1024/-1, Files changed: 5, Commits: 27
|
|
|
|
## Description
|
|
|
|
- Add typed MCP protocol surface in `codex-rs/mcp-server/src/mcp_protocol.rs` for `requests`, `responses`, and `notifications`
|
|
- Requests: `NewConversation`, `Connect`, `SendUserMessage`, `GetConversations`
|
|
- Message content parts: `Text`, `Image` (`ImageUrl`/`FileId`, optional `ImageDetail`), File (`Url`/`Id`/`inline Data`)
|
|
- Responses: `ToolCallResponseEnvelope` with optional `isError` and `structuredContent` variants (`NewConversation`, `Connect`, `SendUserMessageAccepted`, `GetConversations`)
|
|
- Notifications: `InitialState`, `ConnectionRevoked`, `CodexEvent`, `Cancelled`
|
|
- Uniform `_meta` on `notifications` via `NotificationMeta` (`conversationId`, `requestId`)
|
|
- Unit tests validate JSON wire shapes for key `requests`/`responses`/`notifications`
|
|
|
|
## Full Diff
|
|
|
|
```diff
|
|
diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock
|
|
index 653c3e4ef2..9abce0c3db 100644
|
|
--- a/codex-rs/Cargo.lock
|
|
+++ b/codex-rs/Cargo.lock
|
|
@@ -822,6 +822,7 @@ dependencies = [
|
|
"serde",
|
|
"serde_json",
|
|
"shlex",
|
|
+ "strum_macros 0.27.2",
|
|
"tempfile",
|
|
"tokio",
|
|
"tokio-test",
|
|
diff --git a/codex-rs/core/src/config_types.rs b/codex-rs/core/src/config_types.rs
|
|
index 735a571edc..9bf0d483e1 100644
|
|
--- a/codex-rs/core/src/config_types.rs
|
|
+++ b/codex-rs/core/src/config_types.rs
|
|
@@ -78,7 +78,7 @@ pub enum HistoryPersistence {
|
|
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
|
|
pub struct Tui {}
|
|
|
|
-#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Default)]
|
|
+#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Default, Serialize)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub enum SandboxMode {
|
|
#[serde(rename = "read-only")]
|
|
diff --git a/codex-rs/mcp-server/Cargo.toml b/codex-rs/mcp-server/Cargo.toml
|
|
index 488ee6a67c..19cf4db538 100644
|
|
--- a/codex-rs/mcp-server/Cargo.toml
|
|
+++ b/codex-rs/mcp-server/Cargo.toml
|
|
@@ -34,6 +34,7 @@ tokio = { version = "1", features = [
|
|
"signal",
|
|
] }
|
|
uuid = { version = "1", features = ["serde", "v4"] }
|
|
+strum_macros = "0.27.2"
|
|
|
|
[dev-dependencies]
|
|
assert_cmd = "2"
|
|
diff --git a/codex-rs/mcp-server/src/lib.rs b/codex-rs/mcp-server/src/lib.rs
|
|
index aaf67571b4..0912fed118 100644
|
|
--- a/codex-rs/mcp-server/src/lib.rs
|
|
+++ b/codex-rs/mcp-server/src/lib.rs
|
|
@@ -19,6 +19,7 @@ mod codex_tool_config;
|
|
mod codex_tool_runner;
|
|
mod exec_approval;
|
|
mod json_to_toml;
|
|
+mod mcp_protocol;
|
|
mod message_processor;
|
|
mod outgoing_message;
|
|
mod patch_approval;
|
|
diff --git a/codex-rs/mcp-server/src/mcp_protocol.rs b/codex-rs/mcp-server/src/mcp_protocol.rs
|
|
new file mode 100644
|
|
index 0000000000..05eb0a258a
|
|
--- /dev/null
|
|
+++ b/codex-rs/mcp-server/src/mcp_protocol.rs
|
|
@@ -0,0 +1,1020 @@
|
|
+use codex_core::config_types::SandboxMode;
|
|
+use codex_core::protocol::AskForApproval;
|
|
+use codex_core::protocol::EventMsg;
|
|
+use serde::Deserialize;
|
|
+use serde::Serialize;
|
|
+use strum_macros::Display;
|
|
+use uuid::Uuid;
|
|
+
|
|
+use mcp_types::RequestId;
|
|
+
|
|
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
+#[serde(transparent)]
|
|
+pub struct ConversationId(pub Uuid);
|
|
+
|
|
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
+#[serde(transparent)]
|
|
+pub struct MessageId(pub Uuid);
|
|
+
|
|
+// Requests
|
|
+#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
+pub struct ToolCallRequest {
|
|
+ #[serde(rename = "jsonrpc")]
|
|
+ pub jsonrpc: &'static str,
|
|
+ pub id: u64,
|
|
+ pub method: &'static str,
|
|
+ pub params: ToolCallRequestParams,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(tag = "name", content = "arguments", rename_all = "camelCase")]
|
|
+pub enum ToolCallRequestParams {
|
|
+ ConversationCreate(ConversationCreateArgs),
|
|
+ ConversationStream(ConversationStreamArgs),
|
|
+ ConversationSendMessage(ConversationSendMessageArgs),
|
|
+ ConversationsList(ConversationsListArgs),
|
|
+}
|
|
+
|
|
+impl ToolCallRequestParams {
|
|
+ /// Wrap this request in a JSON-RPC request.
|
|
+ #[allow(dead_code)]
|
|
+ pub fn into_request(self, id: u64) -> ToolCallRequest {
|
|
+ ToolCallRequest {
|
|
+ jsonrpc: "2.0",
|
|
+ id,
|
|
+ method: "tools/call",
|
|
+ params: self,
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ConversationCreateArgs {
|
|
+ pub prompt: String,
|
|
+ pub model: String,
|
|
+ pub cwd: String,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub approval_policy: Option<AskForApproval>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub sandbox: Option<SandboxMode>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub config: Option<serde_json::Value>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub profile: Option<String>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub base_instructions: Option<String>,
|
|
+}
|
|
+
|
|
+/// Optional overrides for an existing conversation's execution context when sending a message.
|
|
+/// Fields left as `None` inherit the current conversation/session settings.
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ConversationOverrides {
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub model: Option<String>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub cwd: Option<String>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub approval_policy: Option<AskForApproval>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub sandbox: Option<SandboxMode>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub config: Option<serde_json::Value>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub profile: Option<String>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub base_instructions: Option<String>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ConversationStreamArgs {
|
|
+ pub conversation_id: ConversationId,
|
|
+}
|
|
+
|
|
+/// If omitted, the message continues from the latest turn.
|
|
+/// Set to resume/edit from an earlier parent message in the thread.
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ConversationSendMessageArgs {
|
|
+ pub conversation_id: ConversationId,
|
|
+ pub content: Vec<MessageInputItem>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub parent_message_id: Option<MessageId>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ #[serde(flatten)]
|
|
+ pub conversation_overrides: Option<ConversationOverrides>,
|
|
+}
|
|
+
|
|
+/// Input items for a message.
|
|
+/// Following OpenAI's Responses API: https://platform.openai.com/docs/api-reference/responses
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(tag = "type", rename_all = "camelCase")]
|
|
+pub enum MessageInputItem {
|
|
+ Text {
|
|
+ text: String,
|
|
+ },
|
|
+ Image {
|
|
+ #[serde(flatten)]
|
|
+ source: ImageSource,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ detail: Option<ImageDetail>,
|
|
+ },
|
|
+ File {
|
|
+ #[serde(flatten)]
|
|
+ source: FileSource,
|
|
+ },
|
|
+}
|
|
+
|
|
+/// Source of an image.
|
|
+/// Following OpenAI's API: https://platform.openai.com/docs/guides/images-vision#giving-a-model-images-as-input
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(untagged)]
|
|
+pub enum ImageSource {
|
|
+ ImageUrl { image_url: String },
|
|
+ FileId { file_id: String },
|
|
+}
|
|
+
|
|
+/// Source of a file.
|
|
+/// Following OpenAI's Responses API: https://platform.openai.com/docs/guides/pdf-files?api-mode=responses#uploading-files
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(untagged)]
|
|
+pub enum FileSource {
|
|
+ Url {
|
|
+ file_url: String,
|
|
+ },
|
|
+ Id {
|
|
+ file_id: String,
|
|
+ },
|
|
+ Base64 {
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ filename: Option<String>,
|
|
+ // Base64-encoded file contents.
|
|
+ file_data: String,
|
|
+ },
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(rename_all = "camelCase")]
|
|
+pub enum ImageDetail {
|
|
+ Low,
|
|
+ High,
|
|
+ Auto,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ConversationsListArgs {
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub limit: Option<u32>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub cursor: Option<String>,
|
|
+}
|
|
+
|
|
+// Responses
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(rename_all = "camelCase")]
|
|
+pub struct ToolCallResponse {
|
|
+ pub request_id: RequestId,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub is_error: Option<bool>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub result: Option<ToolCallResponseResult>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(untagged)]
|
|
+pub enum ToolCallResponseResult {
|
|
+ ConversationCreate(ConversationCreateResult),
|
|
+ ConversationStream(ConversationStreamResult),
|
|
+ ConversationSendMessage(ConversationSendMessageResult),
|
|
+ ConversationsList(ConversationsListResult),
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ConversationCreateResult {
|
|
+ pub conversation_id: ConversationId,
|
|
+ pub model: String,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ConversationStreamResult {}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ConversationSendMessageResult {
|
|
+ pub success: bool,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ConversationsListResult {
|
|
+ pub conversations: Vec<ConversationSummary>,
|
|
+ #[serde(skip_serializing_if = "Option::is_none")]
|
|
+ pub next_cursor: Option<String>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ConversationSummary {
|
|
+ pub conversation_id: ConversationId,
|
|
+ pub title: String,
|
|
+}
|
|
+
|
|
+// Notifications
|
|
+#[derive(Debug, Clone, Deserialize, Display)]
|
|
+pub enum ServerNotification {
|
|
+ InitialState(InitialStateNotificationParams),
|
|
+ StreamDisconnected(StreamDisconnectedNotificationParams),
|
|
+ CodexEvent(CodexEventNotificationParams),
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(rename_all = "camelCase")]
|
|
+pub struct NotificationMeta {
|
|
+ #[serde(skip_serializing_if = "Option::is_none")]
|
|
+ pub conversation_id: Option<ConversationId>,
|
|
+ #[serde(skip_serializing_if = "Option::is_none")]
|
|
+ pub request_id: Option<RequestId>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
+pub struct InitialStateNotificationParams {
|
|
+ #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
|
+ pub meta: Option<NotificationMeta>,
|
|
+ pub initial_state: InitialStatePayload,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
+pub struct InitialStatePayload {
|
|
+ #[serde(default)]
|
|
+ pub events: Vec<CodexEventNotificationParams>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct StreamDisconnectedNotificationParams {
|
|
+ #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
|
+ pub meta: Option<NotificationMeta>,
|
|
+ pub reason: String,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
+pub struct CodexEventNotificationParams {
|
|
+ #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
|
+ pub meta: Option<NotificationMeta>,
|
|
+ pub msg: EventMsg,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(rename_all = "camelCase")]
|
|
+pub struct CancelNotificationParams {
|
|
+ pub request_id: RequestId,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub reason: Option<String>,
|
|
+}
|
|
+
|
|
+impl Serialize for ServerNotification {
|
|
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
+ where
|
|
+ S: serde::Serializer,
|
|
+ {
|
|
+ use serde::ser::SerializeMap;
|
|
+
|
|
+ let mut map = serializer.serialize_map(Some(2))?;
|
|
+ match self {
|
|
+ ServerNotification::CodexEvent(p) => {
|
|
+ map.serialize_entry("method", &format!("notifications/{}", p.msg))?;
|
|
+ map.serialize_entry("params", p)?;
|
|
+ }
|
|
+ ServerNotification::InitialState(p) => {
|
|
+ map.serialize_entry("method", "notifications/initial_state")?;
|
|
+ map.serialize_entry("params", p)?;
|
|
+ }
|
|
+ ServerNotification::StreamDisconnected(p) => {
|
|
+ map.serialize_entry("method", "notifications/stream_disconnected")?;
|
|
+ map.serialize_entry("params", p)?;
|
|
+ }
|
|
+ }
|
|
+ map.end()
|
|
+ }
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
+#[serde(tag = "method", content = "params", rename_all = "camelCase")]
|
|
+pub enum ClientNotification {
|
|
+ #[serde(rename = "notifications/cancelled")]
|
|
+ Cancelled(CancelNotificationParams),
|
|
+}
|
|
+
|
|
+#[cfg(test)]
|
|
+#[allow(clippy::expect_used)]
|
|
+#[allow(clippy::unwrap_used)]
|
|
+mod tests {
|
|
+ use super::*;
|
|
+ use pretty_assertions::assert_eq;
|
|
+ use serde::Serialize;
|
|
+ use serde_json::Value;
|
|
+ use serde_json::json;
|
|
+ use uuid::uuid;
|
|
+
|
|
+ fn to_val<T: Serialize>(v: &T) -> Value {
|
|
+ serde_json::to_value(v).expect("serialize to Value")
|
|
+ }
|
|
+
|
|
+ // ----- Requests -----
|
|
+
|
|
+ #[test]
|
|
+ fn serialize_tool_call_request_params_conversation_create_minimal() {
|
|
+ let req = ToolCallRequestParams::ConversationCreate(ConversationCreateArgs {
|
|
+ prompt: "".into(),
|
|
+ model: "o3".into(),
|
|
+ cwd: "/repo".into(),
|
|
+ approval_policy: None,
|
|
+ sandbox: None,
|
|
+ config: None,
|
|
+ profile: None,
|
|
+ base_instructions: None,
|
|
+ });
|
|
+
|
|
+ let observed = to_val(&req.into_request(2));
|
|
+ let expected = json!({
|
|
+ "jsonrpc": "2.0",
|
|
+ "id": 2,
|
|
+ "method": "tools/call",
|
|
+ "params": {
|
|
+ "name": "conversationCreate",
|
|
+ "arguments": {
|
|
+ "prompt": "",
|
|
+ "model": "o3",
|
|
+ "cwd": "/repo"
|
|
+ }
|
|
+ }
|
|
+ });
|
|
+ assert_eq!(observed, expected);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn serialize_tool_call_request_params_conversation_send_message_with_overrides_and_parent_message_id()
|
|
+ {
|
|
+ let req = ToolCallRequestParams::ConversationSendMessage(ConversationSendMessageArgs {
|
|
+ conversation_id: ConversationId(uuid!("d0f6ecbe-84a2-41c1-b23d-b20473b25eab")),
|
|
+ content: vec![
|
|
+ MessageInputItem::Text { text: "Hi".into() },
|
|
+ MessageInputItem::Image {
|
|
+ source: ImageSource::ImageUrl {
|
|
+ image_url: "https://example.com/cat.jpg".into(),
|
|
+ },
|
|
+ detail: Some(ImageDetail::High),
|
|
+ },
|
|
+ MessageInputItem::File {
|
|
+ source: FileSource::Base64 {
|
|
+ filename: Some("notes.txt".into()),
|
|
+ file_data: "Zm9vYmFy".into(),
|
|
+ },
|
|
+ },
|
|
+ ],
|
|
+ parent_message_id: Some(MessageId(uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8"))),
|
|
+ conversation_overrides: Some(ConversationOverrides {
|
|
+ model: Some("o4-mini".into()),
|
|
+ cwd: Some("/workdir".into()),
|
|
+ approval_policy: None,
|
|
+ sandbox: Some(SandboxMode::DangerFullAccess),
|
|
+ config: Some(json!({"temp": 0.2})),
|
|
+ profile: Some("eng".into()),
|
|
+ base_instructions: Some("Be terse".into()),
|
|
+ }),
|
|
+ });
|
|
+
|
|
+ let observed = to_val(&req.into_request(2));
|
|
+ let expected = json!({
|
|
+ "jsonrpc": "2.0",
|
|
+ "id": 2,
|
|
+ "method": "tools/call",
|
|
+ "params": {
|
|
+ "name": "conversationSendMessage",
|
|
+ "arguments": {
|
|
+ "conversation_id": "d0f6ecbe-84a2-41c1-b23d-b20473b25eab",
|
|
+ "content": [
|
|
+ { "type": "text", "text": "Hi" },
|
|
+ { "type": "image", "image_url": "https://example.com/cat.jpg", "detail": "high" },
|
|
+ { "type": "file", "filename": "notes.txt", "file_data": "Zm9vYmFy" }
|
|
+ ],
|
|
+ "parent_message_id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
|
|
+ "model": "o4-mini",
|
|
+ "cwd": "/workdir",
|
|
+ "sandbox": "danger-full-access",
|
|
+ "config": { "temp": 0.2 },
|
|
+ "profile": "eng",
|
|
+ "base_instructions": "Be terse"
|
|
+ }
|
|
+ }
|
|
+ });
|
|
+ assert_eq!(observed, expected);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn serialize_tool_call_request_params_conversations_list_with_opts() {
|
|
+ let req = ToolCallRequestParams::ConversationsList(ConversationsListArgs {
|
|
+ limit: Some(50),
|
|
+ cursor: Some("abc".into()),
|
|
+ });
|
|
+
|
|
+ let observed = to_val(&req.into_request(2));
|
|
+ let expected = json!({
|
|
+ "jsonrpc": "2.0",
|
|
+ "id": 2,
|
|
+ "method": "tools/call",
|
|
+ "params": {
|
|
+ "name": "conversationsList",
|
|
+ "arguments": {
|
|
+ "limit": 50,
|
|
+ "cursor": "abc"
|
|
+ }
|
|
+ }
|
|
+ });
|
|
+ assert_eq!(observed, expected);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn serialize_tool_call_request_params_conversation_stream() {
|
|
+ let req = ToolCallRequestParams::ConversationStream(ConversationStreamArgs {
|
|
+ conversation_id: ConversationId(uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8")),
|
|
+ });
|
|
+
|
|
+ let observed = to_val(&req.into_request(2));
|
|
+ let expected = json!({
|
|
+ "jsonrpc": "2.0",
|
|
+ "id": 2,
|
|
+ "method": "tools/call",
|
|
+ "params": {
|
|
+ "name": "conversationStream",
|
|
+ "arguments": {
|
|
+ "conversation_id": "67e55044-10b1-426f-9247-bb680e5fe0c8"
|
|
+ }
|
|
+ }
|
|
+ });
|
|
+ assert_eq!(observed, expected);
|
|
+ }
|
|
+
|
|
+ // ----- Message inputs / sources -----
|
|
+
|
|
+ #[test]
|
|
+ fn serialize_message_input_image_file_id_auto_detail() {
|
|
+ let item = MessageInputItem::Image {
|
|
+ source: ImageSource::FileId {
|
|
+ file_id: "file_123".into(),
|
|
+ },
|
|
+ detail: Some(ImageDetail::Auto),
|
|
+ };
|
|
+ let observed = to_val(&item);
|
|
+ let expected = json!({
|
|
+ "type": "image",
|
|
+ "file_id": "file_123",
|
|
+ "detail": "auto"
|
|
+ });
|
|
+ assert_eq!(observed, expected);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn serialize_message_input_file_url_and_id_variants() {
|
|
+ let url = MessageInputItem::File {
|
|
+ source: FileSource::Url {
|
|
+ file_url: "https://example.com/a.pdf".into(),
|
|
+ },
|
|
+ };
|
|
+ let id = MessageInputItem::File {
|
|
+ source: FileSource::Id {
|
|
+ file_id: "file_456".into(),
|
|
+ },
|
|
+ };
|
|
+ assert_eq!(
|
|
+ to_val(&url),
|
|
+ json!({"type":"file","file_url":"https://example.com/a.pdf"})
|
|
+ );
|
|
+ assert_eq!(to_val(&id), json!({"type":"file","file_id":"file_456"}));
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn serialize_message_input_image_url_without_detail() {
|
|
+ let item = MessageInputItem::Image {
|
|
+ source: ImageSource::ImageUrl {
|
|
+ image_url: "https://example.com/x.png".into(),
|
|
+ },
|
|
+ detail: None,
|
|
+ };
|
|
+ let observed = to_val(&item);
|
|
+ let expected = json!({
|
|
+ "type": "image",
|
|
+ "image_url": "https://example.com/x.png"
|
|
+ });
|
|
+ assert_eq!(observed, expected);
|
|
+ }
|
|
+
|
|
+ // ----- Responses -----
|
|
+
|
|
+ #[test]
|
|
+ fn response_success_conversation_create_full_schema() {
|
|
+ let env = ToolCallResponse {
|
|
+ request_id: RequestId::Integer(1),
|
|
+ is_error: None,
|
|
+ result: Some(ToolCallResponseResult::ConversationCreate(
|
|
+ ConversationCreateResult {
|
|
+ conversation_id: ConversationId(uuid!("d0f6ecbe-84a2-41c1-b23d-b20473b25eab")),
|
|
+ model: "o3".into(),
|
|
+ },
|
|
+ )),
|
|
+ };
|
|
+ let observed = to_val(&env);
|
|
+ let expected = json!({
|
|
+ "requestId": 1,
|
|
+ "result": {
|
|
+ "conversation_id": "d0f6ecbe-84a2-41c1-b23d-b20473b25eab",
|
|
+ "model": "o3"
|
|
+ }
|
|
+ });
|
|
+ assert_eq!(
|
|
+ observed, expected,
|
|
+ "response (ConversationCreate) must match"
|
|
+ );
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn response_success_conversation_stream_empty_result_object() {
|
|
+ let env = ToolCallResponse {
|
|
+ request_id: RequestId::Integer(2),
|
|
+ is_error: None,
|
|
+ result: Some(ToolCallResponseResult::ConversationStream(
|
|
+ ConversationStreamResult {},
|
|
+ )),
|
|
+ };
|
|
+ let observed = to_val(&env);
|
|
+ let expected = json!({
|
|
+ "requestId": 2,
|
|
+ "result": {}
|
|
+ });
|
|
+ assert_eq!(
|
|
+ observed, expected,
|
|
+ "response (ConversationStream) must have empty object result"
|
|
+ );
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn response_success_send_message_accepted_full_schema() {
|
|
+ let env = ToolCallResponse {
|
|
+ request_id: RequestId::Integer(3),
|
|
+ is_error: None,
|
|
+ result: Some(ToolCallResponseResult::ConversationSendMessage(
|
|
+ ConversationSendMessageResult { success: true },
|
|
+ )),
|
|
+ };
|
|
+ let observed = to_val(&env);
|
|
+ let expected = json!({
|
|
+ "requestId": 3,
|
|
+ "result": { "success": true }
|
|
+ });
|
|
+ assert_eq!(
|
|
+ observed, expected,
|
|
+ "response (ConversationSendMessageAccepted) must match"
|
|
+ );
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn response_success_conversations_list_with_next_cursor_full_schema() {
|
|
+ let env = ToolCallResponse {
|
|
+ request_id: RequestId::Integer(4),
|
|
+ is_error: None,
|
|
+ result: Some(ToolCallResponseResult::ConversationsList(
|
|
+ ConversationsListResult {
|
|
+ conversations: vec![ConversationSummary {
|
|
+ conversation_id: ConversationId(uuid!(
|
|
+ "67e55044-10b1-426f-9247-bb680e5fe0c8"
|
|
+ )),
|
|
+ title: "Refactor config loader".into(),
|
|
+ }],
|
|
+ next_cursor: Some("next123".into()),
|
|
+ },
|
|
+ )),
|
|
+ };
|
|
+ let observed = to_val(&env);
|
|
+ let expected = json!({
|
|
+ "requestId": 4,
|
|
+ "result": {
|
|
+ "conversations": [
|
|
+ {
|
|
+ "conversation_id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
|
|
+ "title": "Refactor config loader"
|
|
+ }
|
|
+ ],
|
|
+ "next_cursor": "next123"
|
|
+ }
|
|
+ });
|
|
+ assert_eq!(
|
|
+ observed, expected,
|
|
+ "response (ConversationsList with cursor) must match"
|
|
+ );
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn response_error_only_is_error_and_request_id_string() {
|
|
+ let env = ToolCallResponse {
|
|
+ request_id: RequestId::Integer(4),
|
|
+ is_error: Some(true),
|
|
+ result: None,
|
|
+ };
|
|
+ let observed = to_val(&env);
|
|
+ let expected = json!({
|
|
+ "requestId": 4,
|
|
+ "isError": true
|
|
+ });
|
|
+ assert_eq!(
|
|
+ observed, expected,
|
|
+ "error response must omit `result` and include `isError`"
|
|
+ );
|
|
+ }
|
|
+
|
|
+ // ----- Notifications -----
|
|
+
|
|
+ #[test]
|
|
+ fn serialize_notification_initial_state_minimal() {
|
|
+ let params = InitialStateNotificationParams {
|
|
+ meta: Some(NotificationMeta {
|
|
+ conversation_id: Some(ConversationId(uuid!(
|
|
+ "67e55044-10b1-426f-9247-bb680e5fe0c8"
|
|
+ ))),
|
|
+ request_id: Some(RequestId::Integer(44)),
|
|
+ }),
|
|
+ initial_state: InitialStatePayload {
|
|
+ events: vec![
|
|
+ CodexEventNotificationParams {
|
|
+ meta: None,
|
|
+ msg: EventMsg::TaskStarted,
|
|
+ },
|
|
+ CodexEventNotificationParams {
|
|
+ meta: None,
|
|
+ msg: EventMsg::AgentMessageDelta(
|
|
+ codex_core::protocol::AgentMessageDeltaEvent {
|
|
+ delta: "Loading...".into(),
|
|
+ },
|
|
+ ),
|
|
+ },
|
|
+ ],
|
|
+ },
|
|
+ };
|
|
+
|
|
+ let observed = to_val(&ServerNotification::InitialState(params.clone()));
|
|
+ let expected = json!({
|
|
+ "method": "notifications/initial_state",
|
|
+ "params": {
|
|
+ "_meta": {
|
|
+ "conversationId": "67e55044-10b1-426f-9247-bb680e5fe0c8",
|
|
+ "requestId": 44
|
|
+ },
|
|
+ "initial_state": {
|
|
+ "events": [
|
|
+ { "msg": { "type": "task_started" } },
|
|
+ { "msg": { "type": "agent_message_delta", "delta": "Loading..." } }
|
|
+ ]
|
|
+ }
|
|
+ }
|
|
+ });
|
|
+ assert_eq!(observed, expected);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn serialize_notification_initial_state_omits_empty_events_full_json() {
|
|
+ let params = InitialStateNotificationParams {
|
|
+ meta: None,
|
|
+ initial_state: InitialStatePayload { events: vec![] },
|
|
+ };
|
|
+
|
|
+ let observed = to_val(&ServerNotification::InitialState(params));
|
|
+ let expected = json!({
|
|
+ "method": "notifications/initial_state",
|
|
+ "params": {
|
|
+ "initial_state": { "events": [] }
|
|
+ }
|
|
+ });
|
|
+ assert_eq!(observed, expected);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn serialize_notification_stream_disconnected() {
|
|
+ let params = StreamDisconnectedNotificationParams {
|
|
+ meta: Some(NotificationMeta {
|
|
+ conversation_id: Some(ConversationId(uuid!(
|
|
+ "67e55044-10b1-426f-9247-bb680e5fe0c8"
|
|
+ ))),
|
|
+ request_id: None,
|
|
+ }),
|
|
+ reason: "New stream() took over".into(),
|
|
+ };
|
|
+
|
|
+ let observed = to_val(&ServerNotification::StreamDisconnected(params));
|
|
+ let expected = json!({
|
|
+ "method": "notifications/stream_disconnected",
|
|
+ "params": {
|
|
+ "_meta": { "conversationId": "67e55044-10b1-426f-9247-bb680e5fe0c8" },
|
|
+ "reason": "New stream() took over"
|
|
+ }
|
|
+ });
|
|
+ assert_eq!(observed, expected);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn serialize_notification_codex_event_uses_eventmsg_type_in_method() {
|
|
+ let params = CodexEventNotificationParams {
|
|
+ meta: Some(NotificationMeta {
|
|
+ conversation_id: Some(ConversationId(uuid!(
|
|
+ "67e55044-10b1-426f-9247-bb680e5fe0c8"
|
|
+ ))),
|
|
+ request_id: Some(RequestId::Integer(44)),
|
|
+ }),
|
|
+ msg: EventMsg::AgentMessage(codex_core::protocol::AgentMessageEvent {
|
|
+ message: "hi".into(),
|
|
+ }),
|
|
+ };
|
|
+
|
|
+ let observed = to_val(&ServerNotification::CodexEvent(params));
|
|
+ let expected = json!({
|
|
+ "method": "notifications/agent_message",
|
|
+ "params": {
|
|
+ "_meta": {
|
|
+ "conversationId": "67e55044-10b1-426f-9247-bb680e5fe0c8",
|
|
+ "requestId": 44
|
|
+ },
|
|
+ "msg": { "type": "agent_message", "message": "hi" }
|
|
+ }
|
|
+ });
|
|
+ assert_eq!(observed, expected);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn serialize_notification_codex_event_task_started_full_json() {
|
|
+ let params = CodexEventNotificationParams {
|
|
+ meta: Some(NotificationMeta {
|
|
+ conversation_id: Some(ConversationId(uuid!(
|
|
+ "67e55044-10b1-426f-9247-bb680e5fe0c8"
|
|
+ ))),
|
|
+ request_id: Some(RequestId::Integer(7)),
|
|
+ }),
|
|
+ msg: EventMsg::TaskStarted,
|
|
+ };
|
|
+
|
|
+ let observed = to_val(&ServerNotification::CodexEvent(params));
|
|
+ let expected = json!({
|
|
+ "method": "notifications/task_started",
|
|
+ "params": {
|
|
+ "_meta": {
|
|
+ "conversationId": "67e55044-10b1-426f-9247-bb680e5fe0c8",
|
|
+ "requestId": 7
|
|
+ },
|
|
+ "msg": { "type": "task_started" }
|
|
+ }
|
|
+ });
|
|
+ assert_eq!(observed, expected);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn serialize_notification_codex_event_agent_message_delta_full_json() {
|
|
+ let params = CodexEventNotificationParams {
|
|
+ meta: None,
|
|
+ msg: EventMsg::AgentMessageDelta(codex_core::protocol::AgentMessageDeltaEvent {
|
|
+ delta: "stream...".into(),
|
|
+ }),
|
|
+ };
|
|
+
|
|
+ let observed = to_val(&ServerNotification::CodexEvent(params));
|
|
+ let expected = json!({
|
|
+ "method": "notifications/agent_message_delta",
|
|
+ "params": {
|
|
+ "msg": { "type": "agent_message_delta", "delta": "stream..." }
|
|
+ }
|
|
+ });
|
|
+ assert_eq!(observed, expected);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn serialize_notification_codex_event_agent_message_full_json() {
|
|
+ let params = CodexEventNotificationParams {
|
|
+ meta: Some(NotificationMeta {
|
|
+ conversation_id: Some(ConversationId(uuid!(
|
|
+ "67e55044-10b1-426f-9247-bb680e5fe0c8"
|
|
+ ))),
|
|
+ request_id: Some(RequestId::Integer(44)),
|
|
+ }),
|
|
+ msg: EventMsg::AgentMessage(codex_core::protocol::AgentMessageEvent {
|
|
+ message: "hi".into(),
|
|
+ }),
|
|
+ };
|
|
+
|
|
+ let observed = to_val(&ServerNotification::CodexEvent(params));
|
|
+ let expected = json!({
|
|
+ "method": "notifications/agent_message",
|
|
+ "params": {
|
|
+ "_meta": {
|
|
+ "conversationId": "67e55044-10b1-426f-9247-bb680e5fe0c8",
|
|
+ "requestId": 44
|
|
+ },
|
|
+ "msg": { "type": "agent_message", "message": "hi" }
|
|
+ }
|
|
+ });
|
|
+ assert_eq!(observed, expected);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn serialize_notification_codex_event_agent_reasoning_full_json() {
|
|
+ let params = CodexEventNotificationParams {
|
|
+ meta: None,
|
|
+ msg: EventMsg::AgentReasoning(codex_core::protocol::AgentReasoningEvent {
|
|
+ text: "thinking…".into(),
|
|
+ }),
|
|
+ };
|
|
+
|
|
+ let observed = to_val(&ServerNotification::CodexEvent(params));
|
|
+ let expected = json!({
|
|
+ "method": "notifications/agent_reasoning",
|
|
+ "params": {
|
|
+ "msg": { "type": "agent_reasoning", "text": "thinking…" }
|
|
+ }
|
|
+ });
|
|
+ assert_eq!(observed, expected);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn serialize_notification_codex_event_token_count_full_json() {
|
|
+ let usage = codex_core::protocol::TokenUsage {
|
|
+ input_tokens: 10,
|
|
+ cached_input_tokens: Some(2),
|
|
+ output_tokens: 5,
|
|
+ reasoning_output_tokens: Some(1),
|
|
+ total_tokens: 16,
|
|
+ };
|
|
+ let params = CodexEventNotificationParams {
|
|
+ meta: None,
|
|
+ msg: EventMsg::TokenCount(usage),
|
|
+ };
|
|
+
|
|
+ let observed = to_val(&ServerNotification::CodexEvent(params));
|
|
+ let expected = json!({
|
|
+ "method": "notifications/token_count",
|
|
+ "params": {
|
|
+ "msg": {
|
|
+ "type": "token_count",
|
|
+ "input_tokens": 10,
|
|
+ "cached_input_tokens": 2,
|
|
+ "output_tokens": 5,
|
|
+ "reasoning_output_tokens": 1,
|
|
+ "total_tokens": 16
|
|
+ }
|
|
+ }
|
|
+ });
|
|
+ assert_eq!(observed, expected);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn serialize_notification_codex_event_session_configured_full_json() {
|
|
+ let params = CodexEventNotificationParams {
|
|
+ meta: Some(NotificationMeta {
|
|
+ conversation_id: Some(ConversationId(uuid!(
|
|
+ "67e55044-10b1-426f-9247-bb680e5fe0c8"
|
|
+ ))),
|
|
+ request_id: None,
|
|
+ }),
|
|
+ msg: EventMsg::SessionConfigured(codex_core::protocol::SessionConfiguredEvent {
|
|
+ session_id: uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8"),
|
|
+ model: "codex-mini-latest".into(),
|
|
+ history_log_id: 42,
|
|
+ history_entry_count: 3,
|
|
+ }),
|
|
+ };
|
|
+
|
|
+ let observed = to_val(&ServerNotification::CodexEvent(params));
|
|
+ let expected = json!({
|
|
+ "method": "notifications/session_configured",
|
|
+ "params": {
|
|
+ "_meta": { "conversationId": "67e55044-10b1-426f-9247-bb680e5fe0c8" },
|
|
+ "msg": {
|
|
+ "type": "session_configured",
|
|
+ "session_id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
|
|
+ "model": "codex-mini-latest",
|
|
+ "history_log_id": 42,
|
|
+ "history_entry_count": 3
|
|
+ }
|
|
+ }
|
|
+ });
|
|
+ assert_eq!(observed, expected);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn serialize_notification_codex_event_exec_command_begin_full_json() {
|
|
+ let params = CodexEventNotificationParams {
|
|
+ meta: None,
|
|
+ msg: EventMsg::ExecCommandBegin(codex_core::protocol::ExecCommandBeginEvent {
|
|
+ call_id: "c1".into(),
|
|
+ command: vec!["bash".into(), "-lc".into(), "echo hi".into()],
|
|
+ cwd: std::path::PathBuf::from("/work"),
|
|
+ }),
|
|
+ };
|
|
+
|
|
+ let observed = to_val(&ServerNotification::CodexEvent(params));
|
|
+ let expected = json!({
|
|
+ "method": "notifications/exec_command_begin",
|
|
+ "params": {
|
|
+ "msg": {
|
|
+ "type": "exec_command_begin",
|
|
+ "call_id": "c1",
|
|
+ "command": ["bash", "-lc", "echo hi"],
|
|
+ "cwd": "/work"
|
|
+ }
|
|
+ }
|
|
+ });
|
|
+ assert_eq!(observed, expected);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn serialize_notification_codex_event_mcp_tool_call_begin_full_json() {
|
|
+ let params = CodexEventNotificationParams {
|
|
+ meta: None,
|
|
+ msg: EventMsg::McpToolCallBegin(codex_core::protocol::McpToolCallBeginEvent {
|
|
+ call_id: "m1".into(),
|
|
+ server: "calc".into(),
|
|
+ tool: "add".into(),
|
|
+ arguments: Some(json!({"a":1,"b":2})),
|
|
+ }),
|
|
+ };
|
|
+
|
|
+ let observed = to_val(&ServerNotification::CodexEvent(params));
|
|
+ let expected = json!({
|
|
+ "method": "notifications/mcp_tool_call_begin",
|
|
+ "params": {
|
|
+ "msg": {
|
|
+ "type": "mcp_tool_call_begin",
|
|
+ "call_id": "m1",
|
|
+ "server": "calc",
|
|
+ "tool": "add",
|
|
+ "arguments": { "a": 1, "b": 2 }
|
|
+ }
|
|
+ }
|
|
+ });
|
|
+ assert_eq!(observed, expected);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn serialize_notification_codex_event_patch_apply_end_full_json() {
|
|
+ let params = CodexEventNotificationParams {
|
|
+ meta: None,
|
|
+ msg: EventMsg::PatchApplyEnd(codex_core::protocol::PatchApplyEndEvent {
|
|
+ call_id: "p1".into(),
|
|
+ stdout: "ok".into(),
|
|
+ stderr: "".into(),
|
|
+ success: true,
|
|
+ }),
|
|
+ };
|
|
+
|
|
+ let observed = to_val(&ServerNotification::CodexEvent(params));
|
|
+ let expected = json!({
|
|
+ "method": "notifications/patch_apply_end",
|
|
+ "params": {
|
|
+ "msg": {
|
|
+ "type": "patch_apply_end",
|
|
+ "call_id": "p1",
|
|
+ "stdout": "ok",
|
|
+ "stderr": "",
|
|
+ "success": true
|
|
+ }
|
|
+ }
|
|
+ });
|
|
+ assert_eq!(observed, expected);
|
|
+ }
|
|
+
|
|
+ // ----- Cancelled notifications -----
|
|
+
|
|
+ #[test]
|
|
+ fn serialize_notification_cancelled_with_reason_full_json() {
|
|
+ let params = CancelNotificationParams {
|
|
+ request_id: RequestId::String("r-123".into()),
|
|
+ reason: Some("user_cancelled".into()),
|
|
+ };
|
|
+
|
|
+ let observed = to_val(&ClientNotification::Cancelled(params));
|
|
+ let expected = json!({
|
|
+ "method": "notifications/cancelled",
|
|
+ "params": {
|
|
+ "requestId": "r-123",
|
|
+ "reason": "user_cancelled"
|
|
+ }
|
|
+ });
|
|
+ assert_eq!(observed, expected);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn serialize_notification_cancelled_without_reason_full_json() {
|
|
+ let params = CancelNotificationParams {
|
|
+ request_id: RequestId::Integer(77),
|
|
+ reason: None,
|
|
+ };
|
|
+
|
|
+ let observed = to_val(&ClientNotification::Cancelled(params));
|
|
+
|
|
+ // Check exact structure: reason must be omitted.
|
|
+ assert_eq!(observed["method"], "notifications/cancelled");
|
|
+ assert_eq!(observed["params"]["requestId"], 77);
|
|
+ assert!(
|
|
+ observed["params"].get("reason").is_none(),
|
|
+ "reason must be omitted when None"
|
|
+ );
|
|
+ }
|
|
+}
|
|
```
|
|
|
|
## Review Comments
|
|
|
|
### codex-rs/mcp-server/src/mcp_protocol.rs
|
|
|
|
- Created: 2025-07-28 22:40:03 UTC | Link: https://github.com/openai/codex/pull/1715#discussion_r2237980204
|
|
|
|
```diff
|
|
@@ -0,0 +1,408 @@
|
|
+use codex_core::protocol::EventMsg;
|
|
+use serde::Deserialize;
|
|
+use serde::Serialize;
|
|
+use uuid::Uuid;
|
|
+
|
|
+use mcp_types::RequestId;
|
|
+
|
|
+// Requests
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(tag = "name", content = "arguments", rename_all = "snake_case")]
|
|
+pub enum ToolCallRequestParams {
|
|
+ NewConversation(NewConversationArgs),
|
|
+ Connect(ConnectArgs),
|
|
+ SendUserMessage(SendUserMessageArgs),
|
|
+ GetConversations(GetConversationsArgs),
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct NewConversationArgs {
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub prompt: Option<String>,
|
|
+ pub model: String,
|
|
+ pub cwd: String,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub approval_policy: Option<String>,
|
|
```
|
|
|
|
> Why is this a `String` instead of the enum value?
|
|
|
|
- Created: 2025-07-28 22:40:11 UTC | Link: https://github.com/openai/codex/pull/1715#discussion_r2237980311
|
|
|
|
```diff
|
|
@@ -0,0 +1,408 @@
|
|
+use codex_core::protocol::EventMsg;
|
|
+use serde::Deserialize;
|
|
+use serde::Serialize;
|
|
+use uuid::Uuid;
|
|
+
|
|
+use mcp_types::RequestId;
|
|
+
|
|
+// Requests
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(tag = "name", content = "arguments", rename_all = "snake_case")]
|
|
+pub enum ToolCallRequestParams {
|
|
+ NewConversation(NewConversationArgs),
|
|
+ Connect(ConnectArgs),
|
|
+ SendUserMessage(SendUserMessageArgs),
|
|
+ GetConversations(GetConversationsArgs),
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct NewConversationArgs {
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub prompt: Option<String>,
|
|
+ pub model: String,
|
|
+ pub cwd: String,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub approval_policy: Option<String>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub sandbox: Option<String>,
|
|
```
|
|
|
|
> Here too.
|
|
|
|
- Created: 2025-07-28 22:46:00 UTC | Link: https://github.com/openai/codex/pull/1715#discussion_r2237985652
|
|
|
|
```diff
|
|
@@ -0,0 +1,408 @@
|
|
+use codex_core::protocol::EventMsg;
|
|
+use serde::Deserialize;
|
|
+use serde::Serialize;
|
|
+use uuid::Uuid;
|
|
+
|
|
+use mcp_types::RequestId;
|
|
+
|
|
+// Requests
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(tag = "name", content = "arguments", rename_all = "snake_case")]
|
|
+pub enum ToolCallRequestParams {
|
|
+ NewConversation(NewConversationArgs),
|
|
+ Connect(ConnectArgs),
|
|
+ SendUserMessage(SendUserMessageArgs),
|
|
+ GetConversations(GetConversationsArgs),
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct NewConversationArgs {
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub prompt: Option<String>,
|
|
+ pub model: String,
|
|
+ pub cwd: String,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub approval_policy: Option<String>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub sandbox: Option<String>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub config: Option<serde_json::Value>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub profile: Option<String>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub base_instructions: Option<String>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ConnectArgs {
|
|
+ pub conversation_id: Uuid,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct SendUserMessageArgs {
|
|
+ pub conversation_id: Uuid,
|
|
```
|
|
|
|
> I am still inclined to use a `u32` instead of a `Uuid` for this.
|
|
>
|
|
> FYI, in Rust, there is a thing called the "newtype" idiom: https://doc.rust-lang.org/rust-by-example/generics/new_types.html
|
|
>
|
|
> In our case, I would be inclined to do:
|
|
>
|
|
> ```
|
|
> struct ConversationId(u32)
|
|
> ```
|
|
>
|
|
> so that we use the `ConversationId` throughout and if we want to change how we represent it, we can do it all in one place.
|
|
>
|
|
> Rust is designed so that there is no extra allocation because this is a "struct:" it just uses the four bytes for the `u32`.
|
|
|
|
- Created: 2025-07-28 22:50:17 UTC | Link: https://github.com/openai/codex/pull/1715#discussion_r2237989776
|
|
|
|
```diff
|
|
@@ -0,0 +1,408 @@
|
|
+use codex_core::protocol::EventMsg;
|
|
+use serde::Deserialize;
|
|
+use serde::Serialize;
|
|
+use uuid::Uuid;
|
|
+
|
|
+use mcp_types::RequestId;
|
|
+
|
|
+// Requests
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(tag = "name", content = "arguments", rename_all = "snake_case")]
|
|
+pub enum ToolCallRequestParams {
|
|
+ NewConversation(NewConversationArgs),
|
|
+ Connect(ConnectArgs),
|
|
+ SendUserMessage(SendUserMessageArgs),
|
|
+ GetConversations(GetConversationsArgs),
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct NewConversationArgs {
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub prompt: Option<String>,
|
|
+ pub model: String,
|
|
+ pub cwd: String,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub approval_policy: Option<String>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub sandbox: Option<String>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub config: Option<serde_json::Value>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub profile: Option<String>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub base_instructions: Option<String>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ConnectArgs {
|
|
+ pub conversation_id: Uuid,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct SendUserMessageArgs {
|
|
+ pub conversation_id: Uuid,
|
|
+ pub content: Vec<InputMessageContentPart>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub message_id: Option<String>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(tag = "type", rename_all = "snake_case")]
|
|
+pub enum InputMessageContentPart {
|
|
+ Text {
|
|
+ text: String,
|
|
+ },
|
|
+ Image {
|
|
+ #[serde(flatten)]
|
|
+ source: ImageSource,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ detail: Option<ImageDetail>,
|
|
+ },
|
|
+ File {
|
|
+ #[serde(flatten)]
|
|
+ source: FileSource,
|
|
+ },
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(untagged)]
|
|
+pub enum ImageSource {
|
|
+ ImageUrl { image_url: String },
|
|
+ FileId { file_id: String },
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(untagged)]
|
|
+pub enum FileSource {
|
|
+ Url {
|
|
+ file_url: String,
|
|
+ },
|
|
+ Id {
|
|
+ file_id: String,
|
|
+ },
|
|
+ Data {
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ filename: Option<String>,
|
|
+ file_data: String,
|
|
+ },
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(rename_all = "snake_case")]
|
|
+pub enum ImageDetail {
|
|
+ Low,
|
|
+ High,
|
|
+ Auto,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct GetConversationsArgs {
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub limit: Option<u32>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub cursor: Option<String>,
|
|
+}
|
|
+
|
|
+// Responses
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ToolCallResponseEnvelope {
|
|
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
+ pub content: Vec<ToolCallResponseContent>,
|
|
+ #[serde(rename = "isError", default, skip_serializing_if = "Option::is_none")]
|
|
+ pub is_error: Option<bool>,
|
|
+ #[serde(
|
|
+ rename = "structuredContent",
|
|
+ default,
|
|
+ skip_serializing_if = "Option::is_none"
|
|
+ )]
|
|
+ pub structured_content: Option<ToolCallResponseData>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(tag = "type", content = "data", rename_all = "snake_case")]
|
|
+pub enum ToolCallResponseData {
|
|
+ NewConversation(NewConversationResult),
|
|
+ Connect(ConnectResult),
|
|
+ SendUserMessage(SendUserMessageAccepted),
|
|
+ GetConversations(GetConversationsResult),
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct NewConversationResult {
|
|
+ pub conversation_id: Uuid,
|
|
+ pub model: String,
|
|
+ pub history_log_id: u64,
|
|
+ pub history_entry_count: usize,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ConnectResult {}
|
|
```
|
|
|
|
> Can you define the result closer to the request? The request is `ConnectArgs` in this case, right?
|
|
>
|
|
> I am probably in the minority, but I would favor using `Uuid` in the request and then "exchanging" it for `u32` in the response.
|
|
|
|
- Created: 2025-07-28 22:51:22 UTC | Link: https://github.com/openai/codex/pull/1715#discussion_r2237990832
|
|
|
|
```diff
|
|
@@ -0,0 +1,408 @@
|
|
+use codex_core::protocol::EventMsg;
|
|
+use serde::Deserialize;
|
|
+use serde::Serialize;
|
|
+use uuid::Uuid;
|
|
+
|
|
+use mcp_types::RequestId;
|
|
+
|
|
+// Requests
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(tag = "name", content = "arguments", rename_all = "snake_case")]
|
|
+pub enum ToolCallRequestParams {
|
|
+ NewConversation(NewConversationArgs),
|
|
+ Connect(ConnectArgs),
|
|
+ SendUserMessage(SendUserMessageArgs),
|
|
+ GetConversations(GetConversationsArgs),
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct NewConversationArgs {
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub prompt: Option<String>,
|
|
+ pub model: String,
|
|
+ pub cwd: String,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub approval_policy: Option<String>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub sandbox: Option<String>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub config: Option<serde_json::Value>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub profile: Option<String>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub base_instructions: Option<String>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ConnectArgs {
|
|
+ pub conversation_id: Uuid,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct SendUserMessageArgs {
|
|
+ pub conversation_id: Uuid,
|
|
+ pub content: Vec<InputMessageContentPart>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub message_id: Option<String>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(tag = "type", rename_all = "snake_case")]
|
|
+pub enum InputMessageContentPart {
|
|
+ Text {
|
|
+ text: String,
|
|
+ },
|
|
+ Image {
|
|
+ #[serde(flatten)]
|
|
+ source: ImageSource,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ detail: Option<ImageDetail>,
|
|
+ },
|
|
+ File {
|
|
+ #[serde(flatten)]
|
|
+ source: FileSource,
|
|
+ },
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(untagged)]
|
|
+pub enum ImageSource {
|
|
+ ImageUrl { image_url: String },
|
|
+ FileId { file_id: String },
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(untagged)]
|
|
+pub enum FileSource {
|
|
+ Url {
|
|
+ file_url: String,
|
|
+ },
|
|
+ Id {
|
|
+ file_id: String,
|
|
+ },
|
|
+ Data {
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ filename: Option<String>,
|
|
+ file_data: String,
|
|
+ },
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(rename_all = "snake_case")]
|
|
+pub enum ImageDetail {
|
|
+ Low,
|
|
+ High,
|
|
+ Auto,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct GetConversationsArgs {
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub limit: Option<u32>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub cursor: Option<String>,
|
|
+}
|
|
+
|
|
+// Responses
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ToolCallResponseEnvelope {
|
|
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
+ pub content: Vec<ToolCallResponseContent>,
|
|
+ #[serde(rename = "isError", default, skip_serializing_if = "Option::is_none")]
|
|
+ pub is_error: Option<bool>,
|
|
+ #[serde(
|
|
+ rename = "structuredContent",
|
|
+ default,
|
|
+ skip_serializing_if = "Option::is_none"
|
|
+ )]
|
|
+ pub structured_content: Option<ToolCallResponseData>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(tag = "type", content = "data", rename_all = "snake_case")]
|
|
+pub enum ToolCallResponseData {
|
|
+ NewConversation(NewConversationResult),
|
|
+ Connect(ConnectResult),
|
|
+ SendUserMessage(SendUserMessageAccepted),
|
|
```
|
|
|
|
> Can/should we use a consistent suffix for all these names? The others are all `Result`?
|
|
|
|
- Created: 2025-07-28 22:53:55 UTC | Link: https://github.com/openai/codex/pull/1715#discussion_r2237993300
|
|
|
|
```diff
|
|
@@ -0,0 +1,408 @@
|
|
+use codex_core::protocol::EventMsg;
|
|
+use serde::Deserialize;
|
|
+use serde::Serialize;
|
|
+use uuid::Uuid;
|
|
+
|
|
+use mcp_types::RequestId;
|
|
+
|
|
+// Requests
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(tag = "name", content = "arguments", rename_all = "snake_case")]
|
|
+pub enum ToolCallRequestParams {
|
|
+ NewConversation(NewConversationArgs),
|
|
+ Connect(ConnectArgs),
|
|
+ SendUserMessage(SendUserMessageArgs),
|
|
+ GetConversations(GetConversationsArgs),
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct NewConversationArgs {
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub prompt: Option<String>,
|
|
+ pub model: String,
|
|
+ pub cwd: String,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub approval_policy: Option<String>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub sandbox: Option<String>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub config: Option<serde_json::Value>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub profile: Option<String>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub base_instructions: Option<String>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ConnectArgs {
|
|
+ pub conversation_id: Uuid,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct SendUserMessageArgs {
|
|
+ pub conversation_id: Uuid,
|
|
+ pub content: Vec<InputMessageContentPart>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub message_id: Option<String>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(tag = "type", rename_all = "snake_case")]
|
|
+pub enum InputMessageContentPart {
|
|
+ Text {
|
|
+ text: String,
|
|
+ },
|
|
+ Image {
|
|
+ #[serde(flatten)]
|
|
+ source: ImageSource,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ detail: Option<ImageDetail>,
|
|
+ },
|
|
+ File {
|
|
+ #[serde(flatten)]
|
|
+ source: FileSource,
|
|
+ },
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(untagged)]
|
|
+pub enum ImageSource {
|
|
+ ImageUrl { image_url: String },
|
|
+ FileId { file_id: String },
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(untagged)]
|
|
+pub enum FileSource {
|
|
+ Url {
|
|
+ file_url: String,
|
|
+ },
|
|
+ Id {
|
|
+ file_id: String,
|
|
+ },
|
|
+ Data {
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ filename: Option<String>,
|
|
+ file_data: String,
|
|
+ },
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(rename_all = "snake_case")]
|
|
+pub enum ImageDetail {
|
|
+ Low,
|
|
+ High,
|
|
+ Auto,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct GetConversationsArgs {
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub limit: Option<u32>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub cursor: Option<String>,
|
|
+}
|
|
+
|
|
+// Responses
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ToolCallResponseEnvelope {
|
|
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
+ pub content: Vec<ToolCallResponseContent>,
|
|
+ #[serde(rename = "isError", default, skip_serializing_if = "Option::is_none")]
|
|
+ pub is_error: Option<bool>,
|
|
+ #[serde(
|
|
+ rename = "structuredContent",
|
|
+ default,
|
|
+ skip_serializing_if = "Option::is_none"
|
|
+ )]
|
|
+ pub structured_content: Option<ToolCallResponseData>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(tag = "type", content = "data", rename_all = "snake_case")]
|
|
+pub enum ToolCallResponseData {
|
|
+ NewConversation(NewConversationResult),
|
|
+ Connect(ConnectResult),
|
|
+ SendUserMessage(SendUserMessageAccepted),
|
|
+ GetConversations(GetConversationsResult),
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct NewConversationResult {
|
|
+ pub conversation_id: Uuid,
|
|
+ pub model: String,
|
|
+ pub history_log_id: u64,
|
|
+ pub history_entry_count: usize,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ConnectResult {}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct SendUserMessageAccepted {
|
|
+ pub accepted: bool,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct GetConversationsResult {
|
|
+ pub conversations: Vec<ConversationSummary>,
|
|
+ #[serde(skip_serializing_if = "Option::is_none")]
|
|
+ pub next_cursor: Option<String>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ConversationSummary {
|
|
+ pub conversation_id: Uuid,
|
|
+ pub title: String,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(tag = "type", rename_all = "snake_case")]
|
|
+pub enum ToolCallResponseContent {
|
|
+ Text { text: String },
|
|
+}
|
|
+
|
|
+// Notifications
|
|
+#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
+#[serde(tag = "type", content = "data", rename_all = "snake_case")]
|
|
+pub enum ConversationNotificationParams {
|
|
```
|
|
|
|
> I think we want one enum for all notifications, not just conversation notifications?
|
|
>
|
|
> On the client side, I think we want to be able to dispatch based on the `type` field.
|
|
|
|
- Created: 2025-07-28 22:55:14 UTC | Link: https://github.com/openai/codex/pull/1715#discussion_r2237994549
|
|
|
|
```diff
|
|
@@ -0,0 +1,408 @@
|
|
+use codex_core::protocol::EventMsg;
|
|
+use serde::Deserialize;
|
|
+use serde::Serialize;
|
|
+use uuid::Uuid;
|
|
+
|
|
+use mcp_types::RequestId;
|
|
+
|
|
+// Requests
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(tag = "name", content = "arguments", rename_all = "snake_case")]
|
|
+pub enum ToolCallRequestParams {
|
|
+ NewConversation(NewConversationArgs),
|
|
+ Connect(ConnectArgs),
|
|
+ SendUserMessage(SendUserMessageArgs),
|
|
+ GetConversations(GetConversationsArgs),
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct NewConversationArgs {
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub prompt: Option<String>,
|
|
+ pub model: String,
|
|
+ pub cwd: String,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub approval_policy: Option<String>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub sandbox: Option<String>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub config: Option<serde_json::Value>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub profile: Option<String>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub base_instructions: Option<String>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ConnectArgs {
|
|
+ pub conversation_id: Uuid,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct SendUserMessageArgs {
|
|
+ pub conversation_id: Uuid,
|
|
+ pub content: Vec<InputMessageContentPart>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub message_id: Option<String>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(tag = "type", rename_all = "snake_case")]
|
|
+pub enum InputMessageContentPart {
|
|
+ Text {
|
|
+ text: String,
|
|
+ },
|
|
+ Image {
|
|
+ #[serde(flatten)]
|
|
+ source: ImageSource,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ detail: Option<ImageDetail>,
|
|
+ },
|
|
+ File {
|
|
+ #[serde(flatten)]
|
|
+ source: FileSource,
|
|
+ },
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(untagged)]
|
|
+pub enum ImageSource {
|
|
+ ImageUrl { image_url: String },
|
|
+ FileId { file_id: String },
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(untagged)]
|
|
+pub enum FileSource {
|
|
+ Url {
|
|
+ file_url: String,
|
|
+ },
|
|
+ Id {
|
|
+ file_id: String,
|
|
+ },
|
|
+ Data {
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ filename: Option<String>,
|
|
+ file_data: String,
|
|
+ },
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(rename_all = "snake_case")]
|
|
+pub enum ImageDetail {
|
|
+ Low,
|
|
+ High,
|
|
+ Auto,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct GetConversationsArgs {
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub limit: Option<u32>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub cursor: Option<String>,
|
|
+}
|
|
+
|
|
+// Responses
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ToolCallResponseEnvelope {
|
|
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
+ pub content: Vec<ToolCallResponseContent>,
|
|
+ #[serde(rename = "isError", default, skip_serializing_if = "Option::is_none")]
|
|
+ pub is_error: Option<bool>,
|
|
+ #[serde(
|
|
+ rename = "structuredContent",
|
|
+ default,
|
|
+ skip_serializing_if = "Option::is_none"
|
|
+ )]
|
|
+ pub structured_content: Option<ToolCallResponseData>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(tag = "type", content = "data", rename_all = "snake_case")]
|
|
+pub enum ToolCallResponseData {
|
|
+ NewConversation(NewConversationResult),
|
|
+ Connect(ConnectResult),
|
|
+ SendUserMessage(SendUserMessageAccepted),
|
|
+ GetConversations(GetConversationsResult),
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct NewConversationResult {
|
|
+ pub conversation_id: Uuid,
|
|
+ pub model: String,
|
|
+ pub history_log_id: u64,
|
|
+ pub history_entry_count: usize,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ConnectResult {}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct SendUserMessageAccepted {
|
|
+ pub accepted: bool,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct GetConversationsResult {
|
|
+ pub conversations: Vec<ConversationSummary>,
|
|
+ #[serde(skip_serializing_if = "Option::is_none")]
|
|
+ pub next_cursor: Option<String>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ConversationSummary {
|
|
+ pub conversation_id: Uuid,
|
|
+ pub title: String,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(tag = "type", rename_all = "snake_case")]
|
|
+pub enum ToolCallResponseContent {
|
|
+ Text { text: String },
|
|
+}
|
|
+
|
|
+// Notifications
|
|
+#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
+#[serde(tag = "type", content = "data", rename_all = "snake_case")]
|
|
+pub enum ConversationNotificationParams {
|
|
+ InitialState(InitialStateNotificationParams),
|
|
+ ConnectionRevoked(ConnectionRevokedNotificationParams),
|
|
+ CodexEvent(CodexEventNotificationParams),
|
|
+ Cancelled(CancelledNotificationParams),
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(rename_all = "camelCase")]
|
|
+pub struct NotificationMeta {
|
|
+ #[serde(skip_serializing_if = "Option::is_none")]
|
|
+ pub conversation_id: Option<Uuid>,
|
|
+ #[serde(skip_serializing_if = "Option::is_none")]
|
|
+ pub request_id: Option<RequestId>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
+pub struct InitialStateNotificationParams {
|
|
+ #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
|
+ pub meta: Option<NotificationMeta>,
|
|
+ pub initial_state: InitialStatePayload,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
+pub struct InitialStatePayload {
|
|
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
+ pub events: Vec<CodexEventNotificationParams>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ConnectionRevokedNotificationParams {
|
|
+ #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
|
+ pub meta: Option<NotificationMeta>,
|
|
+ pub reason: String,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
+pub struct CodexEventNotificationParams {
|
|
+ #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
|
+ pub meta: Option<NotificationMeta>,
|
|
+ pub msg: EventMsg,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct CancelledNotificationParams {
|
|
+ pub id: RequestId,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub reason: Option<String>,
|
|
+}
|
|
+
|
|
+#[cfg(test)]
|
|
+mod tests {
|
|
+ use super::*;
|
|
+ use pretty_assertions::assert_eq;
|
|
+ use serde_json::json;
|
|
+ use uuid::uuid;
|
|
+
|
|
+ #[test]
|
|
+ fn serialize_initial_state_params_minimal() {
|
|
+ let params = InitialStateNotificationParams {
|
|
+ meta: Some(NotificationMeta {
|
|
+ conversation_id: Some(uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8")),
|
|
+ request_id: Some(RequestId::Integer(44)),
|
|
+ }),
|
|
+ initial_state: InitialStatePayload {
|
|
+ events: vec![
|
|
+ CodexEventNotificationParams {
|
|
+ meta: None,
|
|
+ msg: EventMsg::TaskStarted,
|
|
+ },
|
|
+ CodexEventNotificationParams {
|
|
+ meta: None,
|
|
+ msg: EventMsg::AgentMessageDelta(
|
|
+ codex_core::protocol::AgentMessageDeltaEvent {
|
|
+ delta: "Loading...".into(),
|
|
+ },
|
|
+ ),
|
|
+ },
|
|
+ ],
|
|
+ },
|
|
+ };
|
|
+
|
|
+ let got = match serde_json::to_value(¶ms) {
|
|
+ Ok(v) => v,
|
|
+ Err(e) => panic!("failed to serialize InitialStateNotificationParams: {e}"),
|
|
+ };
|
|
+ let expected = json!({
|
|
+ "_meta": {
|
|
+ "conversationId": "67e55044-10b1-426f-9247-bb680e5fe0c8",
|
|
+ "requestId": 44
|
|
+ },
|
|
+ "initial_state": {
|
|
+ "events": [
|
|
+ { "msg": { "type": "task_started" } },
|
|
+ { "msg": { "type": "agent_message_delta", "delta": "Loading..." } }
|
|
+ ]
|
|
+ }
|
|
+ });
|
|
+ assert_eq!(got, expected);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn serialize_connection_revoked_params() {
|
|
+ let params = ConnectionRevokedNotificationParams {
|
|
+ meta: Some(NotificationMeta {
|
|
+ conversation_id: Some(uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8")),
|
|
+ request_id: None,
|
|
+ }),
|
|
+ reason: "New connect() took over".into(),
|
|
+ };
|
|
+ let got = match serde_json::to_value(¶ms) {
|
|
```
|
|
|
|
> Note "observed" is generally the preferred term compared to "got."
|
|
|
|
- Created: 2025-07-28 22:56:01 UTC | Link: https://github.com/openai/codex/pull/1715#discussion_r2237995361
|
|
|
|
```diff
|
|
@@ -0,0 +1,408 @@
|
|
+use codex_core::protocol::EventMsg;
|
|
+use serde::Deserialize;
|
|
+use serde::Serialize;
|
|
+use uuid::Uuid;
|
|
+
|
|
+use mcp_types::RequestId;
|
|
+
|
|
+// Requests
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(tag = "name", content = "arguments", rename_all = "snake_case")]
|
|
+pub enum ToolCallRequestParams {
|
|
+ NewConversation(NewConversationArgs),
|
|
+ Connect(ConnectArgs),
|
|
+ SendUserMessage(SendUserMessageArgs),
|
|
+ GetConversations(GetConversationsArgs),
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct NewConversationArgs {
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub prompt: Option<String>,
|
|
+ pub model: String,
|
|
+ pub cwd: String,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub approval_policy: Option<String>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub sandbox: Option<String>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub config: Option<serde_json::Value>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub profile: Option<String>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub base_instructions: Option<String>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ConnectArgs {
|
|
+ pub conversation_id: Uuid,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct SendUserMessageArgs {
|
|
+ pub conversation_id: Uuid,
|
|
+ pub content: Vec<InputMessageContentPart>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub message_id: Option<String>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(tag = "type", rename_all = "snake_case")]
|
|
+pub enum InputMessageContentPart {
|
|
+ Text {
|
|
+ text: String,
|
|
+ },
|
|
+ Image {
|
|
+ #[serde(flatten)]
|
|
+ source: ImageSource,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ detail: Option<ImageDetail>,
|
|
+ },
|
|
+ File {
|
|
+ #[serde(flatten)]
|
|
+ source: FileSource,
|
|
+ },
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(untagged)]
|
|
+pub enum ImageSource {
|
|
+ ImageUrl { image_url: String },
|
|
+ FileId { file_id: String },
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(untagged)]
|
|
+pub enum FileSource {
|
|
+ Url {
|
|
+ file_url: String,
|
|
+ },
|
|
+ Id {
|
|
+ file_id: String,
|
|
+ },
|
|
+ Data {
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ filename: Option<String>,
|
|
+ file_data: String,
|
|
+ },
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(rename_all = "snake_case")]
|
|
+pub enum ImageDetail {
|
|
+ Low,
|
|
+ High,
|
|
+ Auto,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct GetConversationsArgs {
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub limit: Option<u32>,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub cursor: Option<String>,
|
|
+}
|
|
+
|
|
+// Responses
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ToolCallResponseEnvelope {
|
|
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
+ pub content: Vec<ToolCallResponseContent>,
|
|
+ #[serde(rename = "isError", default, skip_serializing_if = "Option::is_none")]
|
|
+ pub is_error: Option<bool>,
|
|
+ #[serde(
|
|
+ rename = "structuredContent",
|
|
+ default,
|
|
+ skip_serializing_if = "Option::is_none"
|
|
+ )]
|
|
+ pub structured_content: Option<ToolCallResponseData>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(tag = "type", content = "data", rename_all = "snake_case")]
|
|
+pub enum ToolCallResponseData {
|
|
+ NewConversation(NewConversationResult),
|
|
+ Connect(ConnectResult),
|
|
+ SendUserMessage(SendUserMessageAccepted),
|
|
+ GetConversations(GetConversationsResult),
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct NewConversationResult {
|
|
+ pub conversation_id: Uuid,
|
|
+ pub model: String,
|
|
+ pub history_log_id: u64,
|
|
+ pub history_entry_count: usize,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ConnectResult {}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct SendUserMessageAccepted {
|
|
+ pub accepted: bool,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct GetConversationsResult {
|
|
+ pub conversations: Vec<ConversationSummary>,
|
|
+ #[serde(skip_serializing_if = "Option::is_none")]
|
|
+ pub next_cursor: Option<String>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ConversationSummary {
|
|
+ pub conversation_id: Uuid,
|
|
+ pub title: String,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(tag = "type", rename_all = "snake_case")]
|
|
+pub enum ToolCallResponseContent {
|
|
+ Text { text: String },
|
|
+}
|
|
+
|
|
+// Notifications
|
|
+#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
+#[serde(tag = "type", content = "data", rename_all = "snake_case")]
|
|
+pub enum ConversationNotificationParams {
|
|
+ InitialState(InitialStateNotificationParams),
|
|
+ ConnectionRevoked(ConnectionRevokedNotificationParams),
|
|
+ CodexEvent(CodexEventNotificationParams),
|
|
+ Cancelled(CancelledNotificationParams),
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+#[serde(rename_all = "camelCase")]
|
|
+pub struct NotificationMeta {
|
|
+ #[serde(skip_serializing_if = "Option::is_none")]
|
|
+ pub conversation_id: Option<Uuid>,
|
|
+ #[serde(skip_serializing_if = "Option::is_none")]
|
|
+ pub request_id: Option<RequestId>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
+pub struct InitialStateNotificationParams {
|
|
+ #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
|
+ pub meta: Option<NotificationMeta>,
|
|
+ pub initial_state: InitialStatePayload,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
+pub struct InitialStatePayload {
|
|
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
+ pub events: Vec<CodexEventNotificationParams>,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct ConnectionRevokedNotificationParams {
|
|
+ #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
|
+ pub meta: Option<NotificationMeta>,
|
|
+ pub reason: String,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
+pub struct CodexEventNotificationParams {
|
|
+ #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
|
+ pub meta: Option<NotificationMeta>,
|
|
+ pub msg: EventMsg,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
+pub struct CancelledNotificationParams {
|
|
+ pub id: RequestId,
|
|
+ #[serde(default, skip_serializing_if = "Option::is_none")]
|
|
+ pub reason: Option<String>,
|
|
+}
|
|
+
|
|
+#[cfg(test)]
|
|
+mod tests {
|
|
+ use super::*;
|
|
+ use pretty_assertions::assert_eq;
|
|
+ use serde_json::json;
|
|
+ use uuid::uuid;
|
|
+
|
|
+ #[test]
|
|
+ fn serialize_initial_state_params_minimal() {
|
|
+ let params = InitialStateNotificationParams {
|
|
+ meta: Some(NotificationMeta {
|
|
+ conversation_id: Some(uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8")),
|
|
+ request_id: Some(RequestId::Integer(44)),
|
|
+ }),
|
|
+ initial_state: InitialStatePayload {
|
|
+ events: vec![
|
|
+ CodexEventNotificationParams {
|
|
+ meta: None,
|
|
+ msg: EventMsg::TaskStarted,
|
|
+ },
|
|
+ CodexEventNotificationParams {
|
|
+ meta: None,
|
|
+ msg: EventMsg::AgentMessageDelta(
|
|
+ codex_core::protocol::AgentMessageDeltaEvent {
|
|
+ delta: "Loading...".into(),
|
|
+ },
|
|
+ ),
|
|
+ },
|
|
+ ],
|
|
+ },
|
|
+ };
|
|
+
|
|
+ let got = match serde_json::to_value(¶ms) {
|
|
+ Ok(v) => v,
|
|
+ Err(e) => panic!("failed to serialize InitialStateNotificationParams: {e}"),
|
|
```
|
|
|
|
> If you use `expect()` you can still specify the message but avoid the extra lines from the `match`. |