mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:23:33 +00:00
feat(acp): sync upstream protocol updates
This commit is contained in:
Generated
+21
-2
@@ -29,10 +29,16 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "agent-client-protocol"
|
||||
version = "0.0.13"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4255a06cc2414033d1fe4baf1968bcc8f16d7e5814f272b97779b5806d129142"
|
||||
checksum = "cc2526e80463b9742afed4829aedd6ae5632d6db778c6cc1fecb80c960c3521b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
"async-trait",
|
||||
"futures",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"schemars 1.0.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -325,6 +331,18 @@ dependencies = [
|
||||
"wait-timeout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.5.0"
|
||||
@@ -994,6 +1012,7 @@ dependencies = [
|
||||
"rand 0.9.2",
|
||||
"regex-lite",
|
||||
"reqwest",
|
||||
"schemars 0.8.22",
|
||||
"seccompiler",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
|
||||
@@ -22,7 +22,7 @@ codex-apply-patch = { path = "../apply-patch" }
|
||||
codex-file-search = { path = "../file-search" }
|
||||
codex-mcp-client = { path = "../mcp-client" }
|
||||
codex-protocol = { path = "../protocol" }
|
||||
agent-client-protocol = "0.0.13"
|
||||
agent-client-protocol = "0.4.0"
|
||||
dirs = "6"
|
||||
env-flags = "0.1.1"
|
||||
eventsource-stream = "0.2.3"
|
||||
@@ -34,6 +34,7 @@ portable-pty = "0.9.0"
|
||||
rand = "0.9"
|
||||
regex-lite = "0.1.7"
|
||||
reqwest = { version = "0.12", features = ["json", "stream"] }
|
||||
schemars = "0.8.22"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.9"
|
||||
|
||||
+41
-20
@@ -12,6 +12,7 @@ use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::config_types::{ClientTools, McpToolId};
|
||||
use crate::mcp_connection_manager::McpConnectionManager;
|
||||
use crate::protocol::FileChange;
|
||||
use crate::protocol::ReviewDecision;
|
||||
@@ -20,13 +21,13 @@ use crate::util::strip_bash_lc_and_escape;
|
||||
pub(crate) struct AcpFileSystem<'a> {
|
||||
session_id: Uuid,
|
||||
mcp_connection_manager: &'a McpConnectionManager,
|
||||
tools: &'a acp::ClientTools,
|
||||
tools: &'a ClientTools,
|
||||
}
|
||||
|
||||
impl<'a> AcpFileSystem<'a> {
|
||||
pub fn new(
|
||||
session_id: Uuid,
|
||||
tools: &'a acp::ClientTools,
|
||||
tools: &'a ClientTools,
|
||||
mcp_connection_manager: &'a McpConnectionManager,
|
||||
) -> Self {
|
||||
Self {
|
||||
@@ -38,14 +39,15 @@ impl<'a> AcpFileSystem<'a> {
|
||||
|
||||
async fn read_text_file_impl(
|
||||
&self,
|
||||
tool: &acp::McpToolId,
|
||||
tool: &McpToolId,
|
||||
path: &Path,
|
||||
) -> Result<String> {
|
||||
let arguments = acp::ReadTextFileArguments {
|
||||
let arguments = acp::ReadTextFileRequest {
|
||||
session_id: acp::SessionId(self.session_id.to_string().into()),
|
||||
path: path.to_path_buf(),
|
||||
line: None,
|
||||
limit: None,
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let CallToolResult {
|
||||
@@ -66,7 +68,7 @@ impl<'a> AcpFileSystem<'a> {
|
||||
anyhow::bail!("Error reading text file: {:?}", structured_content);
|
||||
}
|
||||
|
||||
let output = serde_json::from_value::<acp::ReadTextFileOutput>(
|
||||
let output = serde_json::from_value::<acp::ReadTextFileResponse>(
|
||||
structured_content.context("No output from read_text_file tool")?,
|
||||
)?;
|
||||
|
||||
@@ -75,14 +77,15 @@ impl<'a> AcpFileSystem<'a> {
|
||||
|
||||
async fn write_text_file_impl(
|
||||
&self,
|
||||
tool: &acp::McpToolId,
|
||||
tool: &McpToolId,
|
||||
path: &Path,
|
||||
content: String,
|
||||
) -> Result<()> {
|
||||
let arguments = acp::WriteTextFileArguments {
|
||||
let arguments = acp::WriteTextFileRequest {
|
||||
session_id: acp::SessionId(self.session_id.to_string().into()),
|
||||
path: path.to_path_buf(),
|
||||
content,
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let CallToolResult {
|
||||
@@ -130,8 +133,8 @@ impl<'a> FileSystem for AcpFileSystem<'a> {
|
||||
}
|
||||
|
||||
pub(crate) async fn request_permission(
|
||||
permission_tool: &acp::McpToolId,
|
||||
tool_call: acp::ToolCall,
|
||||
permission_tool: &McpToolId,
|
||||
tool_call: acp::ToolCallUpdate,
|
||||
session_id: Uuid,
|
||||
mcp_connection_manager: &McpConnectionManager,
|
||||
) -> Result<ReviewDecision> {
|
||||
@@ -139,26 +142,30 @@ pub(crate) async fn request_permission(
|
||||
let approve_id = acp::PermissionOptionId("approve".into());
|
||||
let deny_id = acp::PermissionOptionId("deny".into());
|
||||
|
||||
let arguments = acp::RequestPermissionArguments {
|
||||
let arguments = acp::RequestPermissionRequest {
|
||||
session_id: acp::SessionId(session_id.to_string().into()),
|
||||
tool_call,
|
||||
options: vec![
|
||||
acp::PermissionOption {
|
||||
id: approve_for_session_id.clone(),
|
||||
label: "Approve for Session".into(),
|
||||
name: "Approve for Session".into(),
|
||||
kind: acp::PermissionOptionKind::AllowAlways,
|
||||
meta: None,
|
||||
},
|
||||
acp::PermissionOption {
|
||||
id: approve_id.clone(),
|
||||
label: "Approve".into(),
|
||||
name: "Approve".into(),
|
||||
kind: acp::PermissionOptionKind::AllowOnce,
|
||||
meta: None,
|
||||
},
|
||||
acp::PermissionOption {
|
||||
id: deny_id.clone(),
|
||||
label: "Deny".into(),
|
||||
name: "Deny".into(),
|
||||
kind: acp::PermissionOptionKind::RejectOnce,
|
||||
meta: None,
|
||||
},
|
||||
],
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let CallToolResult {
|
||||
@@ -173,7 +180,7 @@ pub(crate) async fn request_permission(
|
||||
.await?;
|
||||
|
||||
let result = structured_content.context("No output from permission tool")?;
|
||||
let result = serde_json::from_value::<acp::RequestPermissionOutput>(result)?;
|
||||
let result = serde_json::from_value::<acp::RequestPermissionResponse>(result)?;
|
||||
|
||||
use acp::RequestPermissionOutcome::*;
|
||||
let decision = match result.outcome {
|
||||
@@ -188,7 +195,7 @@ pub(crate) async fn request_permission(
|
||||
anyhow::bail!("Unexpected permission option: {}", option_id);
|
||||
}
|
||||
}
|
||||
Canceled => ReviewDecision::Abort,
|
||||
Cancelled => ReviewDecision::Abort,
|
||||
};
|
||||
|
||||
Ok(decision)
|
||||
@@ -201,12 +208,14 @@ pub fn new_execute_tool_call(
|
||||
) -> acp::ToolCall {
|
||||
acp::ToolCall {
|
||||
id: acp::ToolCallId(call_id.into()),
|
||||
label: format!("`{}`", strip_bash_lc_and_escape(command)),
|
||||
title: format!("`{}`", strip_bash_lc_and_escape(command)),
|
||||
kind: acp::ToolKind::Execute,
|
||||
status,
|
||||
content: vec![],
|
||||
locations: vec![],
|
||||
raw_input: None,
|
||||
raw_output: None,
|
||||
meta: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +224,7 @@ pub fn new_patch_tool_call(
|
||||
changes: &HashMap<PathBuf, FileChange>,
|
||||
status: acp::ToolCallStatus,
|
||||
) -> acp::ToolCall {
|
||||
let label = if changes.len() == 1
|
||||
let title = if changes.len() == 1
|
||||
&& let Some((path, change)) = changes.iter().next()
|
||||
{
|
||||
let file_name = path.file_name().unwrap_or_default().display().to_string();
|
||||
@@ -224,12 +233,14 @@ pub fn new_patch_tool_call(
|
||||
FileChange::Delete => {
|
||||
return acp::ToolCall {
|
||||
id: acp::ToolCallId(call_id.into()),
|
||||
label: format!("Delete “`{file_name}`”"),
|
||||
title: format!("Delete “`{file_name}`”"),
|
||||
kind: acp::ToolKind::Delete,
|
||||
status,
|
||||
content: vec![],
|
||||
locations: vec![],
|
||||
raw_input: None,
|
||||
raw_output: None,
|
||||
meta: None,
|
||||
};
|
||||
}
|
||||
FileChange::Update {
|
||||
@@ -240,12 +251,14 @@ pub fn new_patch_tool_call(
|
||||
} if original_content == new_content => {
|
||||
return acp::ToolCall {
|
||||
id: acp::ToolCallId(call_id.into()),
|
||||
label: move_path_label(path, new_path),
|
||||
title: move_path_label(path, new_path),
|
||||
kind: acp::ToolKind::Move,
|
||||
status,
|
||||
content: vec![],
|
||||
locations: vec![],
|
||||
raw_input: None,
|
||||
raw_output: None,
|
||||
meta: None,
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
@@ -267,12 +280,14 @@ pub fn new_patch_tool_call(
|
||||
path: path.clone(),
|
||||
old_text: None,
|
||||
new_text: new_content.clone(),
|
||||
meta: None,
|
||||
},
|
||||
});
|
||||
|
||||
locations.push(acp::ToolCallLocation {
|
||||
path: path.clone(),
|
||||
line: None,
|
||||
meta: None,
|
||||
});
|
||||
}
|
||||
FileChange::Delete => {
|
||||
@@ -299,17 +314,20 @@ pub fn new_patch_tool_call(
|
||||
locations.push(acp::ToolCallLocation {
|
||||
path: new_path.clone(),
|
||||
line: None,
|
||||
meta: None,
|
||||
});
|
||||
} else {
|
||||
locations.push(acp::ToolCallLocation {
|
||||
path: path.clone(),
|
||||
line: None,
|
||||
meta: None,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
locations.push(acp::ToolCallLocation {
|
||||
path: path.clone(),
|
||||
line: None,
|
||||
meta: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -319,6 +337,7 @@ pub fn new_patch_tool_call(
|
||||
path: path.clone(),
|
||||
old_text: Some(original_content.clone()),
|
||||
new_text: new_content.clone(),
|
||||
meta: None,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -328,12 +347,14 @@ pub fn new_patch_tool_call(
|
||||
|
||||
acp::ToolCall {
|
||||
id: acp::ToolCallId(call_id.into()),
|
||||
label,
|
||||
title,
|
||||
kind: acp::ToolKind::Edit,
|
||||
status,
|
||||
content,
|
||||
locations,
|
||||
raw_input: None,
|
||||
raw_output: None,
|
||||
meta: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ use codex_apply_patch::MaybeApplyPatchVerified;
|
||||
use codex_apply_patch::StdFileSystem;
|
||||
use codex_browser::BrowserConfig as CodexBrowserConfig;
|
||||
use codex_browser::BrowserManager;
|
||||
use agent_client_protocol::ClientTools;
|
||||
use crate::config_types::ClientTools;
|
||||
// unused: AuthManager
|
||||
// unused: ConversationHistoryResponseEvent
|
||||
use codex_protocol::protocol::TurnAbortReason;
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::config_types::AgentConfig;
|
||||
use crate::config_types::AllowedCommand;
|
||||
use crate::config_types::AllowedCommandMatchKind;
|
||||
use crate::config_types::BrowserConfig;
|
||||
use crate::config_types::ClientTools;
|
||||
use crate::config_types::History;
|
||||
use crate::config_types::GithubConfig;
|
||||
use crate::config_types::ValidationConfig;
|
||||
@@ -20,7 +21,6 @@ use crate::config_types::ShellEnvironmentPolicyToml;
|
||||
use crate::config_types::TextVerbosity;
|
||||
use crate::config_types::Tui;
|
||||
use crate::config_types::UriBasedFileOpener;
|
||||
use agent_client_protocol::ClientTools;
|
||||
use crate::config_types::ConfirmGuardConfig;
|
||||
use crate::git_info::resolve_root_git_project_for_trust;
|
||||
use crate::model_family::ModelFamily;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use schemars::JsonSchema;
|
||||
use wildmatch::WildMatchPattern;
|
||||
|
||||
use shlex::split as shlex_split;
|
||||
@@ -157,6 +158,26 @@ pub struct SubagentsToml {
|
||||
pub commands: Vec<SubagentCommandConfig>,
|
||||
}
|
||||
|
||||
/// MCP tool identifiers that the client exposes to the agent.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ClientTools {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub request_permission: Option<McpToolId>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub write_text_file: Option<McpToolId>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub read_text_file: Option<McpToolId>,
|
||||
}
|
||||
|
||||
/// Identifier for a client-hosted MCP tool.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct McpToolId {
|
||||
pub mcp_server: String,
|
||||
pub tool_name: String,
|
||||
}
|
||||
|
||||
/// Configuration for external agent models
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
|
||||
@@ -5,7 +5,7 @@ This document describes Codex’s experimental MCP interface: a JSON‑RPC API t
|
||||
- Status: experimental and subject to change without notice
|
||||
- Recommended entry point: `code mcp` (alias: `code acp`; both wrap the same binary used by the CLI)
|
||||
- Underlying Rust binary (for development): `cargo run -p code-mcp-server`
|
||||
- ACP compatibility: surfaces `acp/new_session` + `acp/prompt` as MCP tools and emits `acp/session_update` notifications
|
||||
- ACP compatibility: surfaces `session/new` + `session/prompt` as MCP tools and emits `session/update` notifications
|
||||
- Transport: standard MCP over stdio (JSON‑RPC 2.0, line‑delimited)
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -16,7 +16,7 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
agent-client-protocol = "0.0.13"
|
||||
agent-client-protocol = "0.4.0"
|
||||
codex-arg0 = { path = "../arg0" }
|
||||
codex-common = { path = "../common", features = ["cli"] }
|
||||
codex-core = { path = "../core" }
|
||||
|
||||
@@ -13,6 +13,7 @@ use codex_core::config::Config as CodexConfig;
|
||||
use codex_core::protocol::EventMsg;
|
||||
use codex_core::protocol::InputItem;
|
||||
use codex_core::protocol::Op;
|
||||
use codex_protocol::protocol::TurnAbortReason;
|
||||
use mcp_types::CallToolResult;
|
||||
use mcp_types::ContentBlock;
|
||||
use mcp_types::RequestId;
|
||||
@@ -68,7 +69,7 @@ pub async fn prompt(
|
||||
codex: Arc<CodexConversation>,
|
||||
prompt: Vec<acp::ContentBlock>,
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
) -> Result<()> {
|
||||
) -> Result<acp::StopReason> {
|
||||
let items: Vec<InputItem> = prompt
|
||||
.into_iter()
|
||||
.filter_map(acp_content_block_to_item)
|
||||
@@ -79,6 +80,8 @@ pub async fn prompt(
|
||||
.await
|
||||
.context("failed to submit prompt to Codex")?;
|
||||
|
||||
let mut stop_reason = acp::StopReason::EndTurn;
|
||||
|
||||
loop {
|
||||
let event = codex.next_event().await?;
|
||||
|
||||
@@ -97,12 +100,14 @@ pub async fn prompt(
|
||||
let invocation = event.invocation.clone();
|
||||
Some(acp::SessionUpdate::ToolCall(acp::ToolCall {
|
||||
id: acp::ToolCallId(event.call_id.into()),
|
||||
label: format!("{}: {}", invocation.server, invocation.tool),
|
||||
title: format!("{}: {}", invocation.server, invocation.tool),
|
||||
kind: acp::ToolKind::Other,
|
||||
status: acp::ToolCallStatus::InProgress,
|
||||
content: vec![],
|
||||
locations: vec![],
|
||||
raw_input: invocation.arguments,
|
||||
raw_output: None,
|
||||
meta: None,
|
||||
}))
|
||||
}
|
||||
EventMsg::McpToolCallEnd(event) => {
|
||||
@@ -117,6 +122,11 @@ pub async fn prompt(
|
||||
),
|
||||
Err(err) => Some(vec![err.into()]),
|
||||
};
|
||||
let raw_output = event
|
||||
.result
|
||||
.as_ref()
|
||||
.ok()
|
||||
.and_then(|result| result.structured_content.clone());
|
||||
Some(acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate {
|
||||
id: call_id,
|
||||
fields: ToolCallUpdateFields {
|
||||
@@ -126,8 +136,10 @@ pub async fn prompt(
|
||||
Some(acp::ToolCallStatus::Failed)
|
||||
},
|
||||
content,
|
||||
raw_output,
|
||||
..Default::default()
|
||||
},
|
||||
meta: None,
|
||||
}))
|
||||
}
|
||||
EventMsg::ExecApprovalRequest(_) | EventMsg::ApplyPatchApprovalRequest(_) => None,
|
||||
@@ -151,6 +163,7 @@ pub async fn prompt(
|
||||
content: Some(vec![event.stdout.into(), event.stderr.into()]),
|
||||
..Default::default()
|
||||
},
|
||||
meta: None,
|
||||
}))
|
||||
}
|
||||
EventMsg::PatchApplyBegin(event) => Some(acp::SessionUpdate::ToolCall(
|
||||
@@ -172,9 +185,16 @@ pub async fn prompt(
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
meta: None,
|
||||
}))
|
||||
}
|
||||
EventMsg::TaskComplete(_) => return Ok(()),
|
||||
EventMsg::TurnAborted(event) => {
|
||||
if matches!(event.reason, TurnAbortReason::Interrupted) {
|
||||
stop_reason = acp::StopReason::Cancelled;
|
||||
}
|
||||
None
|
||||
}
|
||||
EventMsg::TaskComplete(_) => return Ok(stop_reason),
|
||||
EventMsg::SessionConfigured(_)
|
||||
| EventMsg::TokenCount(_)
|
||||
| EventMsg::TaskStarted
|
||||
@@ -186,11 +206,12 @@ pub async fn prompt(
|
||||
|
||||
if let Some(update) = acp_update {
|
||||
let notification = OutgoingNotification {
|
||||
method: acp::AGENT_METHODS.session_update.to_string(),
|
||||
method: acp::CLIENT_METHOD_NAMES.session_update.to_string(),
|
||||
params: Some(
|
||||
serde_json::to_value(acp::SessionNotification {
|
||||
session_id: acp_session_id.clone(),
|
||||
update,
|
||||
meta: None,
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
@@ -228,6 +249,7 @@ fn to_acp_annotations(annotations: mcp_types::Annotations) -> acp::Annotations {
|
||||
}),
|
||||
last_modified: annotations.last_modified,
|
||||
priority: annotations.priority,
|
||||
meta: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,6 +262,7 @@ fn to_acp_embedded_resource_resource(
|
||||
mime_type: text_contents.mime_type,
|
||||
text: text_contents.text,
|
||||
uri: text_contents.uri,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
mcp_types::EmbeddedResourceResource::BlobResourceContents(blob_contents) => {
|
||||
@@ -247,6 +270,7 @@ fn to_acp_embedded_resource_resource(
|
||||
blob: blob_contents.blob,
|
||||
mime_type: blob_contents.mime_type,
|
||||
uri: blob_contents.uri,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -257,16 +281,20 @@ fn to_acp_content_block(block: mcp_types::ContentBlock) -> acp::ContentBlock {
|
||||
ContentBlock::TextContent(text_content) => acp::ContentBlock::Text(acp::TextContent {
|
||||
annotations: text_content.annotations.map(to_acp_annotations),
|
||||
text: text_content.text,
|
||||
meta: None,
|
||||
}),
|
||||
ContentBlock::ImageContent(image_content) => acp::ContentBlock::Image(acp::ImageContent {
|
||||
annotations: image_content.annotations.map(to_acp_annotations),
|
||||
data: image_content.data,
|
||||
mime_type: image_content.mime_type,
|
||||
uri: None,
|
||||
meta: None,
|
||||
}),
|
||||
ContentBlock::AudioContent(audio_content) => acp::ContentBlock::Audio(acp::AudioContent {
|
||||
annotations: audio_content.annotations.map(to_acp_annotations),
|
||||
data: audio_content.data,
|
||||
mime_type: audio_content.mime_type,
|
||||
meta: None,
|
||||
}),
|
||||
ContentBlock::ResourceLink(resource_link) => {
|
||||
acp::ContentBlock::ResourceLink(acp::ResourceLink {
|
||||
@@ -277,12 +305,14 @@ fn to_acp_content_block(block: mcp_types::ContentBlock) -> acp::ContentBlock {
|
||||
name: resource_link.name,
|
||||
size: resource_link.size,
|
||||
title: resource_link.title,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
ContentBlock::EmbeddedResource(embedded_resource) => {
|
||||
acp::ContentBlock::Resource(acp::EmbeddedResource {
|
||||
annotations: embedded_resource.annotations.map(to_acp_annotations),
|
||||
resource: to_acp_embedded_resource_resource(embedded_resource.resource),
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Configuration object accepted by the `codex` MCP tool-call.
|
||||
|
||||
use agent_client_protocol as acp;
|
||||
use codex_core::config_types::ClientTools;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
use mcp_types::Tool;
|
||||
@@ -12,6 +13,7 @@ use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::json_to_toml::json_to_toml;
|
||||
|
||||
@@ -134,22 +136,24 @@ pub(crate) fn create_tool_for_codex_tool_call_param() -> Tool {
|
||||
|
||||
/// Builds a `Tool` definition for the `acp/new_session` tool-call.
|
||||
pub(crate) fn create_tool_for_acp_new_session() -> Tool {
|
||||
#[expect(clippy::expect_used)]
|
||||
let input_schema_value = serde_json::to_value(acp::NewSessionArguments::schema())
|
||||
.expect("ACP new_session input schema must serialise to JSON");
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
let output_schema_value = serde_json::to_value(acp::NewSessionOutput::schema())
|
||||
.expect("ACP new_session output schema must serialise to JSON");
|
||||
|
||||
let input_schema = serde_json::from_value::<ToolInputSchema>(input_schema_value)
|
||||
.unwrap_or_else(|e| panic!("failed to create Tool input schema: {e}"));
|
||||
let output_schema = serde_json::from_value::<ToolOutputSchema>(output_schema_value)
|
||||
.unwrap_or_else(|e| panic!("failed to create Tool output schema: {e}"));
|
||||
let input_schema = ToolInputSchema {
|
||||
r#type: "object".to_string(),
|
||||
required: Some(vec!["cwd".to_string()]),
|
||||
properties: Some(json!({
|
||||
"cwd": {"type": "string"},
|
||||
"mcpServers": {"type": "array"},
|
||||
"clientTools": {"type": "object"}
|
||||
})),
|
||||
};
|
||||
let output_schema = ToolOutputSchema {
|
||||
r#type: "object".to_string(),
|
||||
properties: None,
|
||||
required: None,
|
||||
};
|
||||
|
||||
Tool {
|
||||
name: acp::AGENT_METHODS.new_session.to_string(),
|
||||
title: Some(acp::AGENT_METHODS.new_session.to_string()),
|
||||
name: acp::AGENT_METHOD_NAMES.session_new.to_string(),
|
||||
title: Some(acp::AGENT_METHOD_NAMES.session_new.to_string()),
|
||||
input_schema,
|
||||
output_schema: Some(output_schema),
|
||||
description: Some("Start a Codex session over ACP.".to_string()),
|
||||
@@ -159,16 +163,19 @@ pub(crate) fn create_tool_for_acp_new_session() -> Tool {
|
||||
|
||||
/// Builds a `Tool` definition for the `acp/prompt` tool-call.
|
||||
pub(crate) fn create_tool_for_acp_prompt() -> Tool {
|
||||
#[expect(clippy::expect_used)]
|
||||
let input_schema_value = serde_json::to_value(acp::PromptArguments::schema())
|
||||
.expect("ACP prompt input schema must serialise to JSON");
|
||||
|
||||
let input_schema = serde_json::from_value::<ToolInputSchema>(input_schema_value)
|
||||
.unwrap_or_else(|e| panic!("failed to create Tool input schema: {e}"));
|
||||
let input_schema = ToolInputSchema {
|
||||
r#type: "object".to_string(),
|
||||
required: Some(vec!["sessionId".to_string(), "prompt".to_string()]),
|
||||
properties: Some(json!({
|
||||
"sessionId": {"type": "string"},
|
||||
"prompt": {"type": "array"},
|
||||
"meta": {"type": "object"}
|
||||
})),
|
||||
};
|
||||
|
||||
Tool {
|
||||
name: acp::AGENT_METHODS.prompt.to_string(),
|
||||
title: Some(acp::AGENT_METHODS.prompt.to_string()),
|
||||
name: acp::AGENT_METHOD_NAMES.session_prompt.to_string(),
|
||||
title: Some(acp::AGENT_METHOD_NAMES.session_prompt.to_string()),
|
||||
input_schema,
|
||||
output_schema: None,
|
||||
description: Some("Send a prompt to an existing ACP Codex session.".to_string()),
|
||||
@@ -176,6 +183,25 @@ pub(crate) fn create_tool_for_acp_prompt() -> Tool {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AcpNewSessionToolArgs {
|
||||
#[serde(flatten)]
|
||||
pub request: serde_json::Value,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub client_tools: Option<ClientTools>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AcpPromptToolArgs {
|
||||
#[serde(rename = "sessionId")]
|
||||
pub session_id: acp::SessionId,
|
||||
pub prompt: Vec<acp::ContentBlock>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl CodexToolCallParam {
|
||||
/// Returns the initial user prompt to start the Codex conversation and the
|
||||
/// effective Config object generated from the supplied parameters.
|
||||
|
||||
@@ -2,15 +2,19 @@ use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::codex_message_processor::CodexMessageProcessor;
|
||||
use crate::codex_tool_config::CodexToolCallParam;
|
||||
use crate::codex_tool_config::CodexToolCallReplyParam;
|
||||
use crate::codex_tool_config::create_tool_for_codex_tool_call_param;
|
||||
use crate::codex_tool_config::create_tool_for_codex_tool_call_reply_param;
|
||||
use crate::codex_tool_config::create_tool_for_acp_new_session;
|
||||
use crate::codex_tool_config::create_tool_for_acp_prompt;
|
||||
use crate::codex_tool_config::create_tool_for_codex_tool_call_param;
|
||||
use crate::codex_tool_config::create_tool_for_codex_tool_call_reply_param;
|
||||
use crate::codex_tool_config::AcpNewSessionToolArgs;
|
||||
use crate::codex_tool_config::AcpPromptToolArgs;
|
||||
use crate::codex_tool_config::CodexToolCallParam;
|
||||
use crate::codex_tool_config::CodexToolCallReplyParam;
|
||||
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
|
||||
use crate::error_code::INTERNAL_ERROR_CODE;
|
||||
use crate::outgoing_message::OutgoingMessageSender;
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Context as _;
|
||||
use codex_protocol::mcp_protocol::ClientRequest;
|
||||
use codex_protocol::mcp_protocol::ConversationId;
|
||||
@@ -18,11 +22,13 @@ use codex_protocol::mcp_protocol::ConversationId;
|
||||
use codex_core::AuthManager;
|
||||
use codex_core::ConversationManager;
|
||||
use codex_core::config_types::McpServerConfig;
|
||||
use codex_core::config_types::ClientTools;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::default_client::USER_AGENT_SUFFIX;
|
||||
use codex_core::default_client::get_codex_user_agent_default;
|
||||
use codex_core::CodexConversation;
|
||||
use codex_core::protocol::Submission;
|
||||
use codex_core::protocol::Op;
|
||||
use codex_protocol::mcp_protocol::AuthMode;
|
||||
use mcp_types::CallToolRequestParams;
|
||||
use mcp_types::CallToolResult;
|
||||
@@ -38,7 +44,6 @@ use mcp_types::ModelContextProtocolRequest;
|
||||
use mcp_types::RequestId;
|
||||
use mcp_types::ServerNotification;
|
||||
use mcp_types::TextContent;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -103,13 +108,14 @@ impl MessageProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
tracing::trace!("processing JSON-RPC request: {}", request.method);
|
||||
// Hold on to the ID so we can respond.
|
||||
let request_id = request.id.clone();
|
||||
|
||||
if request.method == "session/new" {
|
||||
if request.method == acp::AGENT_METHOD_NAMES.session_new {
|
||||
tracing::info!("handling session/new via ACP shim");
|
||||
if let Some(params) = request.params.clone() {
|
||||
match serde_json::from_value::<SessionNewParams>(params) {
|
||||
match serde_json::from_value::<AcpNewSessionToolArgs>(params) {
|
||||
Ok(session_params) => {
|
||||
self.handle_session_new(request_id, session_params)
|
||||
.await;
|
||||
@@ -135,6 +141,35 @@ impl MessageProcessor {
|
||||
return;
|
||||
}
|
||||
|
||||
if request.method == acp::AGENT_METHOD_NAMES.session_prompt {
|
||||
tracing::info!("handling session/prompt via ACP shim");
|
||||
if let Some(params) = request.params.clone() {
|
||||
match serde_json::from_value::<AcpPromptToolArgs>(params) {
|
||||
Ok(prompt_params) => {
|
||||
self.handle_session_prompt(request_id, prompt_params)
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to parse session/prompt params: {err}");
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("invalid session/prompt params: {err}"),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: "session/prompt requires params".to_string(),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let mut request = request;
|
||||
|
||||
if request.method == mcp_types::InitializeRequest::METHOD {
|
||||
@@ -240,6 +275,23 @@ impl MessageProcessor {
|
||||
|
||||
/// Handle a fire-and-forget JSON-RPC notification.
|
||||
pub(crate) async fn process_notification(&mut self, notification: JSONRPCNotification) {
|
||||
if notification.method == acp::AGENT_METHOD_NAMES.session_cancel {
|
||||
tracing::info!("handling session/cancel via ACP shim");
|
||||
if let Some(params) = notification.params {
|
||||
match serde_json::from_value::<acp::CancelNotification>(params) {
|
||||
Ok(cancel) => {
|
||||
self.handle_session_cancel(cancel).await;
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to parse session/cancel params: {err}");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::warn!("session/cancel notification missing params");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let server_notification = match ServerNotification::try_from(notification) {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
@@ -451,10 +503,10 @@ impl MessageProcessor {
|
||||
self.handle_tool_call_codex_session_reply(id, arguments)
|
||||
.await
|
||||
}
|
||||
_ if name == acp::AGENT_METHODS.new_session => {
|
||||
_ if name == acp::AGENT_METHOD_NAMES.session_new => {
|
||||
self.handle_tool_call_acp_new_session(id, arguments).await
|
||||
}
|
||||
_ if name == acp::AGENT_METHODS.prompt => {
|
||||
_ if name == acp::AGENT_METHOD_NAMES.session_prompt => {
|
||||
self.handle_tool_call_acp_prompt(id, arguments).await
|
||||
}
|
||||
_ => {
|
||||
@@ -750,7 +802,7 @@ impl MessageProcessor {
|
||||
request_id: RequestId,
|
||||
arguments: Option<serde_json::Value>,
|
||||
) {
|
||||
let config = match Self::acp_new_session_cfg(arguments) {
|
||||
let config = match self.acp_new_session_cfg(arguments) {
|
||||
Ok(cfg) => cfg,
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to construct new session config: {}", err);
|
||||
@@ -786,21 +838,14 @@ impl MessageProcessor {
|
||||
};
|
||||
|
||||
let session_id_str = session_id.to_string();
|
||||
let modes_payload = serde_json::json!({
|
||||
"currentModeId": "default",
|
||||
"availableModes": [
|
||||
{
|
||||
"id": "default",
|
||||
"name": "Default",
|
||||
"description": "Code prompts before executing tools or applying patches."
|
||||
}
|
||||
]
|
||||
});
|
||||
let response_struct = acp::NewSessionResponse {
|
||||
session_id: acp::SessionId(Arc::from(session_id_str.clone())),
|
||||
modes: Some(default_session_modes()),
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let structured = serde_json::json!({
|
||||
"sessionId": session_id_str,
|
||||
"modes": modes_payload
|
||||
});
|
||||
let structured = serde_json::to_value(response_struct)
|
||||
.unwrap_or_else(|_| json!({ "sessionId": session_id_str }));
|
||||
|
||||
let response = CallToolResult {
|
||||
content: vec![],
|
||||
@@ -812,50 +857,22 @@ impl MessageProcessor {
|
||||
});
|
||||
}
|
||||
|
||||
fn acp_new_session_cfg(arguments: Option<serde_json::Value>) -> anyhow::Result<Config> {
|
||||
fn acp_new_session_cfg(
|
||||
&self,
|
||||
arguments: Option<serde_json::Value>,
|
||||
) -> anyhow::Result<Config> {
|
||||
let arguments = arguments.context("Arguments required")?;
|
||||
let arguments = serde_json::from_value::<acp::NewSessionArguments>(arguments)?;
|
||||
|
||||
let mcp_servers: HashMap<String, McpServerConfig> = arguments
|
||||
.mcp_servers
|
||||
.into_iter()
|
||||
.map(|cfg| {
|
||||
let env: HashMap<String, String> = cfg
|
||||
.env
|
||||
.into_iter()
|
||||
.map(|var| (var.name, var.value))
|
||||
.collect();
|
||||
(
|
||||
cfg.name,
|
||||
McpServerConfig {
|
||||
command: cfg.command.display().to_string(),
|
||||
args: cfg.args,
|
||||
env: if env.is_empty() { None } else { Some(env) },
|
||||
startup_timeout_ms: None,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let overrides = codex_core::config::ConfigOverrides {
|
||||
cwd: Some(arguments.cwd),
|
||||
mcp_servers: Some(mcp_servers),
|
||||
experimental_client_tools: Some(arguments.client_tools),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let cfg =
|
||||
codex_core::config::Config::load_with_cli_overrides(Default::default(), overrides)?;
|
||||
|
||||
Ok(cfg)
|
||||
let arguments = serde_json::from_value::<AcpNewSessionToolArgs>(arguments)?;
|
||||
let request = serde_json::from_value::<acp::NewSessionRequest>(arguments.request)?;
|
||||
self.build_new_session_config(request, arguments.client_tools)
|
||||
}
|
||||
|
||||
async fn handle_session_new(
|
||||
&self,
|
||||
request_id: RequestId,
|
||||
params: SessionNewParams,
|
||||
params: AcpNewSessionToolArgs,
|
||||
) {
|
||||
let config = match self.session_new_config(params).await {
|
||||
let config = match self.session_new_config(params) {
|
||||
Ok(cfg) => cfg,
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to prepare session config: {err}");
|
||||
@@ -886,59 +903,119 @@ impl MessageProcessor {
|
||||
return;
|
||||
};
|
||||
|
||||
let response = serde_json::json!({
|
||||
"sessionId": session_id.to_string(),
|
||||
"modes": {
|
||||
"currentModeId": "default",
|
||||
"availableModes": [
|
||||
{
|
||||
"id": "default",
|
||||
"name": "Default",
|
||||
"description": "Code prompts before executing tools or applying patches."
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
let response = acp::NewSessionResponse {
|
||||
session_id: acp::SessionId(Arc::from(session_id.to_string())),
|
||||
modes: Some(default_session_modes()),
|
||||
meta: None,
|
||||
};
|
||||
|
||||
outgoing.send_response(request_id, response).await;
|
||||
let value = serde_json::to_value(response)
|
||||
.unwrap_or_else(|_| json!({ "sessionId": session_id.to_string() }));
|
||||
|
||||
outgoing.send_response(request_id, value).await;
|
||||
});
|
||||
}
|
||||
|
||||
async fn session_new_config(&self, params: SessionNewParams) -> anyhow::Result<Config> {
|
||||
let mcp_servers: HashMap<String, McpServerConfig> = params
|
||||
.mcp_servers
|
||||
.into_iter()
|
||||
.map(|server| {
|
||||
let env: HashMap<String, String> = server
|
||||
.env
|
||||
.into_iter()
|
||||
.map(|var| (var.name, var.value))
|
||||
.collect();
|
||||
fn session_new_config(&self, params: AcpNewSessionToolArgs) -> anyhow::Result<Config> {
|
||||
let request = serde_json::from_value::<acp::NewSessionRequest>(params.request)?;
|
||||
self.build_new_session_config(request, params.client_tools)
|
||||
}
|
||||
|
||||
(
|
||||
server.name,
|
||||
McpServerConfig {
|
||||
command: server.command.display().to_string(),
|
||||
args: server.args,
|
||||
env: if env.is_empty() { None } else { Some(env) },
|
||||
startup_timeout_ms: None,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let base_tools = self.base_config.experimental_client_tools.clone();
|
||||
fn build_new_session_config(
|
||||
&self,
|
||||
request: acp::NewSessionRequest,
|
||||
override_tools: Option<ClientTools>,
|
||||
) -> anyhow::Result<Config> {
|
||||
let mcp_servers = convert_mcp_servers(request.mcp_servers)?;
|
||||
let client_tools = override_tools
|
||||
.or_else(|| self.base_config.experimental_client_tools.clone());
|
||||
|
||||
let overrides = codex_core::config::ConfigOverrides {
|
||||
cwd: Some(params.cwd),
|
||||
cwd: Some(request.cwd),
|
||||
mcp_servers: Some(mcp_servers),
|
||||
experimental_client_tools: base_tools,
|
||||
experimental_client_tools: client_tools,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Ok(Config::load_with_cli_overrides(Default::default(), overrides)?)
|
||||
}
|
||||
|
||||
async fn handle_session_prompt(
|
||||
&self,
|
||||
request_id: RequestId,
|
||||
params: AcpPromptToolArgs,
|
||||
) {
|
||||
let acp_session_id = params.session_id;
|
||||
let session_uuid = match Uuid::parse_str(&acp_session_id.to_string()) {
|
||||
Ok(id) => id,
|
||||
Err(err) => {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("invalid session id: {err}"),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let session = {
|
||||
let map = self.session_map.lock().await;
|
||||
map.get(&session_uuid).cloned()
|
||||
};
|
||||
|
||||
let Some(session) = session else {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("unknown session id: {}", acp_session_id),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
};
|
||||
|
||||
let outgoing = self.outgoing.clone();
|
||||
let requests_codex_map = self.running_requests_id_to_codex_uuid.clone();
|
||||
let prompt_blocks = params.prompt;
|
||||
|
||||
task::spawn(async move {
|
||||
requests_codex_map
|
||||
.lock()
|
||||
.await
|
||||
.insert(request_id.clone(), session_uuid);
|
||||
|
||||
let result = crate::acp_tool_runner::prompt(
|
||||
acp_session_id.clone(),
|
||||
session,
|
||||
prompt_blocks,
|
||||
outgoing.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(stop_reason) => {
|
||||
let response = acp::PromptResponse {
|
||||
stop_reason,
|
||||
meta: None,
|
||||
};
|
||||
let value = serde_json::to_value(response)
|
||||
.unwrap_or_else(|_| json!({ "stopReason": "end_turn" }));
|
||||
outgoing.send_response(request_id.clone(), value).await;
|
||||
}
|
||||
Err(err) => {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: err.to_string(),
|
||||
data: None,
|
||||
};
|
||||
outgoing.send_error(request_id.clone(), error).await;
|
||||
}
|
||||
}
|
||||
|
||||
requests_codex_map.lock().await.remove(&request_id);
|
||||
});
|
||||
}
|
||||
|
||||
async fn handle_tool_call_acp_prompt(
|
||||
&self,
|
||||
request_id: RequestId,
|
||||
@@ -994,11 +1071,19 @@ impl MessageProcessor {
|
||||
let result = crate::acp_tool_runner::prompt(acp_session_id, session, prompt, outgoing.clone()).await;
|
||||
|
||||
let result = match result {
|
||||
Ok(()) => CallToolResult {
|
||||
content: vec![],
|
||||
is_error: Some(false),
|
||||
structured_content: None,
|
||||
},
|
||||
Ok(stop_reason) => {
|
||||
let structured = serde_json::to_value(acp::PromptResponse {
|
||||
stop_reason,
|
||||
meta: None,
|
||||
})
|
||||
.unwrap_or_else(|_| json!({ "stopReason": "end_turn" }));
|
||||
|
||||
CallToolResult {
|
||||
content: vec![],
|
||||
is_error: Some(false),
|
||||
structured_content: Some(structured),
|
||||
}
|
||||
}
|
||||
Err(err) => CallToolResult {
|
||||
content: vec![ContentBlock::TextContent(TextContent {
|
||||
annotations: None,
|
||||
@@ -1020,10 +1105,10 @@ impl MessageProcessor {
|
||||
arguments: Option<serde_json::Value>,
|
||||
) -> anyhow::Result<(Uuid, acp::SessionId, Vec<acp::ContentBlock>)> {
|
||||
let arguments = arguments.context("Arguments required")?;
|
||||
let arguments = serde_json::from_value::<acp::PromptArguments>(arguments)?;
|
||||
let arguments = serde_json::from_value::<AcpPromptToolArgs>(arguments)?;
|
||||
|
||||
let session_id = Uuid::parse_str(&arguments.session_id.0)?;
|
||||
Ok((session_id, arguments.session_id, arguments.prompt))
|
||||
let session_uuid = Uuid::parse_str(&arguments.session_id.to_string())?;
|
||||
Ok((session_uuid, arguments.session_id, arguments.prompt))
|
||||
}
|
||||
|
||||
fn handle_resource_updated(
|
||||
@@ -1040,6 +1125,63 @@ impl MessageProcessor {
|
||||
tracing::info!("notifications/prompts/list_changed -> params: {:?}", params);
|
||||
}
|
||||
|
||||
async fn handle_session_cancel(&self, params: acp::CancelNotification) {
|
||||
let session_uuid = match Uuid::parse_str(¶ms.session_id.to_string()) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(err) => {
|
||||
tracing::warn!("received session/cancel with invalid session id: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let conversation = {
|
||||
let map = self.session_map.lock().await;
|
||||
map.get(&session_uuid).cloned()
|
||||
};
|
||||
|
||||
let Some(conversation) = conversation else {
|
||||
tracing::warn!("session/cancel for unknown session: {}", params.session_id);
|
||||
return;
|
||||
};
|
||||
|
||||
let request_ids: Vec<RequestId> = {
|
||||
let map = self.running_requests_id_to_codex_uuid.lock().await;
|
||||
map.iter()
|
||||
.filter_map(|(request_id, uuid)| if *uuid == session_uuid {
|
||||
Some(request_id.clone())
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
if request_ids.is_empty() {
|
||||
if let Err(err) = conversation
|
||||
.submit_with_id(Submission {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
op: Op::Interrupt,
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::error!("failed to interrupt session {}: {err}", params.session_id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for request_id in request_ids {
|
||||
let submission_id = request_id_to_string(&request_id);
|
||||
if let Err(err) = conversation
|
||||
.submit_with_id(Submission {
|
||||
id: submission_id,
|
||||
op: Op::Interrupt,
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::error!("failed to interrupt in-flight request: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_tool_list_changed(
|
||||
&self,
|
||||
params: <mcp_types::ToolListChangedNotification as mcp_types::ModelContextProtocolNotification>::Params,
|
||||
@@ -1055,27 +1197,66 @@ fn handle_logging_message(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SessionNewParams {
|
||||
cwd: PathBuf,
|
||||
#[serde(default, rename = "mcpServers")]
|
||||
mcp_servers: Vec<SessionNewMcpServer>,
|
||||
fn convert_mcp_servers(
|
||||
servers: Vec<acp::McpServer>,
|
||||
) -> anyhow::Result<HashMap<String, McpServerConfig>> {
|
||||
let mut map = HashMap::with_capacity(servers.len());
|
||||
for server in servers {
|
||||
match server {
|
||||
acp::McpServer::Stdio { name, command, args, env } => {
|
||||
let env_map: HashMap<String, String> = env
|
||||
.into_iter()
|
||||
.map(|var| (var.name, var.value))
|
||||
.collect();
|
||||
let env_map = if env_map.is_empty() { None } else { Some(env_map) };
|
||||
|
||||
map.insert(
|
||||
name,
|
||||
McpServerConfig {
|
||||
command: command.display().to_string(),
|
||||
args,
|
||||
env: env_map,
|
||||
startup_timeout_ms: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
acp::McpServer::Http { name, .. } => {
|
||||
return Err(anyhow!(
|
||||
"unsupported MCP transport for server '{}': HTTP servers are not yet supported",
|
||||
name
|
||||
));
|
||||
}
|
||||
acp::McpServer::Sse { name, .. } => {
|
||||
return Err(anyhow!(
|
||||
"unsupported MCP transport for server '{}': SSE servers are not yet supported",
|
||||
name
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SessionNewMcpServer {
|
||||
name: String,
|
||||
command: PathBuf,
|
||||
#[serde(default)]
|
||||
args: Vec<String>,
|
||||
#[serde(default)]
|
||||
env: Vec<SessionNewEnvVar>,
|
||||
fn default_session_modes() -> acp::SessionModeState {
|
||||
let mode_id = acp::SessionModeId(Arc::from("default".to_string()));
|
||||
let mode = acp::SessionMode {
|
||||
id: mode_id.clone(),
|
||||
name: "Default".to_string(),
|
||||
description: Some("Code prompts before executing tools or applying patches.".to_string()),
|
||||
meta: None,
|
||||
};
|
||||
|
||||
acp::SessionModeState {
|
||||
current_mode_id: mode_id,
|
||||
available_modes: vec![mode],
|
||||
meta: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SessionNewEnvVar {
|
||||
name: String,
|
||||
value: String,
|
||||
fn request_id_to_string(request_id: &RequestId) -> String {
|
||||
match request_id {
|
||||
RequestId::String(value) => value.clone(),
|
||||
RequestId::Integer(value) => value.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,12 +260,10 @@ pub(crate) struct OutgoingError {
|
||||
mod tests {
|
||||
use codex_core::protocol::EventMsg;
|
||||
use codex_core::protocol::SessionConfiguredEvent;
|
||||
use codex_protocol::config_types::ReasoningEffort;
|
||||
use codex_protocol::mcp_protocol::ConversationId;
|
||||
use codex_protocol::mcp_protocol::LoginChatGptCompleteNotification;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use tempfile::NamedTempFile;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::*;
|
||||
@@ -276,17 +274,16 @@ mod tests {
|
||||
let outgoing_message_sender = OutgoingMessageSender::new(outgoing_tx);
|
||||
|
||||
let conversation_id = ConversationId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let session_uuid: Uuid = conversation_id.into();
|
||||
let event = Event {
|
||||
id: "1".to_string(),
|
||||
event_seq: 0,
|
||||
order: None,
|
||||
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
|
||||
session_id: conversation_id,
|
||||
session_id: session_uuid,
|
||||
model: "gpt-4o".to_string(),
|
||||
reasoning_effort: Some(ReasoningEffort::default()),
|
||||
history_log_id: 1,
|
||||
history_entry_count: 1000,
|
||||
initial_messages: None,
|
||||
rollout_path: rollout_file.path().to_path_buf(),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -312,18 +309,17 @@ mod tests {
|
||||
let outgoing_message_sender = OutgoingMessageSender::new(outgoing_tx);
|
||||
|
||||
let conversation_id = ConversationId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let session_uuid: Uuid = conversation_id.into();
|
||||
let session_configured_event = SessionConfiguredEvent {
|
||||
session_id: conversation_id,
|
||||
session_id: session_uuid,
|
||||
model: "gpt-4o".to_string(),
|
||||
reasoning_effort: Some(ReasoningEffort::default()),
|
||||
history_log_id: 1,
|
||||
history_entry_count: 1000,
|
||||
initial_messages: None,
|
||||
rollout_path: rollout_file.path().to_path_buf(),
|
||||
};
|
||||
let event = Event {
|
||||
id: "1".to_string(),
|
||||
event_seq: 0,
|
||||
order: None,
|
||||
msg: EventMsg::SessionConfigured(session_configured_event.clone()),
|
||||
};
|
||||
let meta = OutgoingNotificationMeta {
|
||||
@@ -344,14 +340,13 @@ mod tests {
|
||||
"requestId": "123",
|
||||
},
|
||||
"id": "1",
|
||||
"event_seq": 0,
|
||||
"msg": {
|
||||
"session_id": session_configured_event.session_id,
|
||||
"model": session_configured_event.model,
|
||||
"reasoning_effort": session_configured_event.reasoning_effort,
|
||||
"history_log_id": session_configured_event.history_log_id,
|
||||
"history_entry_count": session_configured_event.history_entry_count,
|
||||
"type": "session_configured",
|
||||
"rollout_path": rollout_file.path().to_path_buf(),
|
||||
"type": "session_configured"
|
||||
}
|
||||
});
|
||||
assert_eq!(params.unwrap(), expected_params);
|
||||
|
||||
@@ -121,7 +121,6 @@ async fn get_auth_status_with_api_key() {
|
||||
let status: GetAuthStatusResponse = to_response(resp).expect("deserialize status");
|
||||
assert_eq!(status.auth_method, Some(AuthMode::ApiKey));
|
||||
assert_eq!(status.auth_token, Some("sk-test-key".to_string()));
|
||||
assert_eq!(status.preferred_auth_method, AuthMode::ChatGPT);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -130,6 +129,8 @@ async fn get_auth_status_with_api_key_when_auth_not_required() {
|
||||
create_config_toml_custom_provider(codex_home.path(), false)
|
||||
.unwrap_or_else(|err| panic!("write config.toml: {err}"));
|
||||
|
||||
login_with_api_key(codex_home.path(), "sk-test-key").expect("seed api key");
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path())
|
||||
.await
|
||||
.expect("spawn mcp process");
|
||||
@@ -138,8 +139,6 @@ async fn get_auth_status_with_api_key_when_auth_not_required() {
|
||||
.expect("init timeout")
|
||||
.expect("init failed");
|
||||
|
||||
login_with_api_key_via_request(&mut mcp, "sk-test-key").await;
|
||||
|
||||
let request_id = mcp
|
||||
.send_get_auth_status_request(GetAuthStatusParams {
|
||||
include_token: Some(true),
|
||||
@@ -199,5 +198,4 @@ async fn get_auth_status_with_api_key_no_include_token() {
|
||||
let status: GetAuthStatusResponse = to_response(resp).expect("deserialize status");
|
||||
assert_eq!(status.auth_method, Some(AuthMode::ApiKey));
|
||||
assert!(status.auth_token.is_none(), "token must be omitted");
|
||||
assert_eq!(status.preferred_auth_method, AuthMode::ChatGPT);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::path::Path;
|
||||
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_core::protocol_config_types::ReasoningEffort;
|
||||
use codex_core::protocol_config_types::ReasoningSummary;
|
||||
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||||
@@ -90,6 +90,7 @@ async fn test_codex_jsonrpc_conversation_flow() {
|
||||
let NewConversationResponse {
|
||||
conversation_id,
|
||||
model,
|
||||
..
|
||||
} = new_conv_resp;
|
||||
assert_eq!(model, "mock-model");
|
||||
|
||||
|
||||
@@ -65,8 +65,8 @@ async fn tools_list_exposes_acp_entries() -> anyhow::Result<()> {
|
||||
for required in [
|
||||
"codex",
|
||||
"codex-reply",
|
||||
acp::AGENT_METHODS.new_session,
|
||||
acp::AGENT_METHODS.prompt,
|
||||
acp::AGENT_METHOD_NAMES.session_new,
|
||||
acp::AGENT_METHOD_NAMES.session_prompt,
|
||||
] {
|
||||
assert!(
|
||||
tool_names.contains(required),
|
||||
@@ -232,20 +232,15 @@ async fn acp_prompt_round_trip() -> anyhow::Result<()> {
|
||||
dir: _dir,
|
||||
} = create_mcp_process(responses).await?;
|
||||
|
||||
let new_session_args = acp::NewSessionArguments {
|
||||
mcp_servers: Vec::new(),
|
||||
client_tools: acp::ClientTools {
|
||||
request_permission: None,
|
||||
write_text_file: None,
|
||||
read_text_file: None,
|
||||
},
|
||||
cwd: temp_cwd.path().to_path_buf(),
|
||||
};
|
||||
let new_session_args = json!({
|
||||
"cwd": temp_cwd.path(),
|
||||
"mcpServers": []
|
||||
});
|
||||
|
||||
let new_session_request_id = process
|
||||
.send_tool_call(
|
||||
acp::AGENT_METHODS.new_session,
|
||||
Some(serde_json::to_value(new_session_args)?),
|
||||
acp::AGENT_METHOD_NAMES.session_new,
|
||||
Some(new_session_args),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -258,19 +253,27 @@ async fn acp_prompt_round_trip() -> anyhow::Result<()> {
|
||||
let structured = new_session_result
|
||||
.structured_content
|
||||
.context("new_session should return structured content")?;
|
||||
let new_session_output: acp::NewSessionOutput = serde_json::from_value(structured)?;
|
||||
let new_session_output: serde_json::Value = serde_json::from_value(structured)?;
|
||||
let session_id_value = new_session_output
|
||||
.get("sessionId")
|
||||
.and_then(|v| v.as_str())
|
||||
.context("sessionId should be present")?
|
||||
.to_string();
|
||||
let session_id = acp::SessionId(session_id_value.clone().into());
|
||||
|
||||
let prompt_args = acp::PromptArguments {
|
||||
session_id: new_session_output.session_id.clone(),
|
||||
let prompt_args = acp::PromptRequest {
|
||||
session_id: session_id.clone(),
|
||||
prompt: vec![acp::ContentBlock::Text(acp::TextContent {
|
||||
annotations: None,
|
||||
text: "Hello from ACP".to_string(),
|
||||
meta: None,
|
||||
})],
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let prompt_request_id = process
|
||||
.send_tool_call(
|
||||
acp::AGENT_METHODS.prompt,
|
||||
acp::AGENT_METHOD_NAMES.session_prompt,
|
||||
Some(serde_json::to_value(prompt_args)?),
|
||||
)
|
||||
.await?;
|
||||
@@ -281,7 +284,7 @@ async fn acp_prompt_round_trip() -> anyhow::Result<()> {
|
||||
let message = timeout(DEFAULT_READ_TIMEOUT, process.read_jsonrpc_message()).await??;
|
||||
match message {
|
||||
JSONRPCMessage::Notification(notification) => {
|
||||
if notification.method == acp::AGENT_METHODS.session_update {
|
||||
if notification.method == acp::CLIENT_METHOD_NAMES.session_update {
|
||||
saw_session_update = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_protocol::config_types::ConfigProfile;
|
||||
use codex_protocol::config_types::ReasoningEffort;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
use codex_protocol::mcp_protocol::GetConfigTomlResponse;
|
||||
use mcp_test_support::McpProcess;
|
||||
use mcp_test_support::to_response;
|
||||
use mcp_types::JSONRPCResponse;
|
||||
use mcp_types::RequestId;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
@@ -60,21 +55,20 @@ async fn get_config_toml_returns_subset() {
|
||||
.expect("getConfigToml timeout")
|
||||
.expect("getConfigToml response");
|
||||
|
||||
let config: GetConfigTomlResponse = to_response(resp).expect("deserialize config");
|
||||
let expected = GetConfigTomlResponse {
|
||||
approval_policy: Some(AskForApproval::OnRequest),
|
||||
sandbox_mode: Some(SandboxMode::WorkspaceWrite),
|
||||
model_reasoning_effort: Some(ReasoningEffort::High),
|
||||
profile: Some("test".to_string()),
|
||||
profiles: Some(HashMap::from([(
|
||||
"test".into(),
|
||||
ConfigProfile {
|
||||
model: Some("gpt-4o".into()),
|
||||
approval_policy: Some(AskForApproval::OnRequest),
|
||||
model_reasoning_effort: Some(ReasoningEffort::High),
|
||||
},
|
||||
)])),
|
||||
};
|
||||
let config: serde_json::Value = to_response(resp).expect("deserialize config");
|
||||
let expected = json!({
|
||||
"approval_policy": "on-request",
|
||||
"sandbox_mode": "workspace-write",
|
||||
"model_reasoning_effort": "high",
|
||||
"profile": "test",
|
||||
"profiles": {
|
||||
"test": {
|
||||
"model": "gpt-4o",
|
||||
"approval_policy": "on-request",
|
||||
"model_reasoning_effort": "high"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(expected, config);
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ async fn test_conversation_create_and_send_message_ok() {
|
||||
let NewConversationResponse {
|
||||
conversation_id,
|
||||
model,
|
||||
..
|
||||
} = to_response::<NewConversationResponse>(new_conv_resp)
|
||||
.expect("deserialize newConversation response");
|
||||
assert_eq!(model, "o3");
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use codex_core::protocol::TurnAbortReason;
|
||||
use codex_protocol::protocol::TurnAbortReason;
|
||||
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||||
use codex_protocol::mcp_protocol::AddConversationListenerParams;
|
||||
use codex_protocol::mcp_protocol::InterruptConversationParams;
|
||||
|
||||
@@ -13,9 +13,43 @@ const UNUSED_CELL: &str = "░░";
|
||||
const BAR_SLOTS: usize = 20;
|
||||
const BAR_FILLED: &str = "▰";
|
||||
const BAR_EMPTY: &str = "▱";
|
||||
const SECTION_INDENT: &str = " ";
|
||||
const FIELD_INDENT: &str = " ";
|
||||
const CHART_INDENT: &str = " ";
|
||||
struct IndentSpec {
|
||||
header: &'static str,
|
||||
label_extra: &'static str,
|
||||
chart_extra: &'static str,
|
||||
label_target_width: usize,
|
||||
label_gap: usize,
|
||||
}
|
||||
|
||||
const INDENTS: IndentSpec = IndentSpec {
|
||||
header: "",
|
||||
label_extra: " ",
|
||||
chart_extra: " ",
|
||||
label_target_width: 7,
|
||||
label_gap: 2,
|
||||
};
|
||||
|
||||
fn header_indent() -> &'static str {
|
||||
INDENTS.header
|
||||
}
|
||||
|
||||
fn label_indent() -> String {
|
||||
format!("{}{}", INDENTS.header, INDENTS.label_extra)
|
||||
}
|
||||
|
||||
fn chart_indent() -> String {
|
||||
format!("{}{}", INDENTS.header, INDENTS.chart_extra)
|
||||
}
|
||||
|
||||
fn chart_indent_width() -> usize {
|
||||
INDENTS.header.len() + INDENTS.chart_extra.len()
|
||||
}
|
||||
|
||||
fn label_text(text: &str) -> String {
|
||||
let mut result = label_indent();
|
||||
result.push_str(text);
|
||||
result
|
||||
}
|
||||
|
||||
/// Aggregated output used by the `/limits` command.
|
||||
/// It contains the rendered summary lines, optional legend,
|
||||
@@ -177,8 +211,11 @@ fn build_footer_lines(metrics: &RateLimitMetrics) -> Vec<Line<'static>> {
|
||||
}
|
||||
|
||||
fn section_header(title: &str) -> Line<'static> {
|
||||
let mut text = String::with_capacity(header_indent().len() + title.len());
|
||||
text.push_str(header_indent());
|
||||
text.push_str(title);
|
||||
Line::from(vec![Span::styled(
|
||||
format!("{SECTION_INDENT}{title}"),
|
||||
text,
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
)])
|
||||
}
|
||||
@@ -252,31 +289,34 @@ fn build_hourly_reset_line(
|
||||
last_reset: Option<DateTime<Utc>>,
|
||||
) -> Line<'static> {
|
||||
if let (Some(last), true) = (last_reset, window_minutes > 0) {
|
||||
let prefix = field_prefix("Resets");
|
||||
if let Some(timing) = compute_window_timing(window_minutes, last) {
|
||||
let remaining = format_duration(timing.remaining);
|
||||
return Line::from(vec![
|
||||
Span::raw(format!("{FIELD_INDENT}Resets:")),
|
||||
Span::raw(" "),
|
||||
Span::raw("at "),
|
||||
Span::raw(format!("{}", timing.next_reset_local)),
|
||||
Span::styled(
|
||||
format!(" (in {remaining})"),
|
||||
Style::default().fg(colors::dim()),
|
||||
),
|
||||
]);
|
||||
let mut spans: Vec<Span<'static>> = Vec::new();
|
||||
spans.push(Span::raw(prefix.clone()));
|
||||
spans.push(Span::raw("at "));
|
||||
spans.push(Span::raw(timing.next_reset_local));
|
||||
spans.push(Span::styled(
|
||||
format!(" (in {remaining})"),
|
||||
Style::default().fg(colors::dim()),
|
||||
));
|
||||
return Line::from(spans);
|
||||
}
|
||||
return Line::from(vec![Span::raw(format!(
|
||||
"{FIELD_INDENT}Reset timing updating…"
|
||||
))]);
|
||||
return Line::from(vec![
|
||||
Span::raw(prefix),
|
||||
Span::styled(
|
||||
"timing updating…".to_string(),
|
||||
Style::default().fg(colors::dim()),
|
||||
),
|
||||
]);
|
||||
}
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{FIELD_INDENT}Resets:")),
|
||||
Span::raw(" "),
|
||||
Span::styled(
|
||||
"shown once next window detected".to_string(),
|
||||
Style::default().fg(colors::dim()),
|
||||
),
|
||||
])
|
||||
let mut spans: Vec<Span<'static>> = Vec::new();
|
||||
spans.push(Span::raw(field_prefix("Resets")));
|
||||
spans.push(Span::styled(
|
||||
"shown once next window detected".to_string(),
|
||||
Style::default().fg(colors::dim()),
|
||||
));
|
||||
Line::from(spans)
|
||||
}
|
||||
|
||||
fn build_weekly_window_line(
|
||||
@@ -335,31 +375,34 @@ fn build_weekly_reset_line(
|
||||
last_reset: Option<DateTime<Utc>>,
|
||||
) -> Line<'static> {
|
||||
if let (Some(last), true) = (last_reset, window_minutes > 0) {
|
||||
let prefix = field_prefix("Resets");
|
||||
if let Some(timing) = compute_window_timing(window_minutes, last) {
|
||||
let remaining = format_duration(timing.remaining);
|
||||
return Line::from(vec![
|
||||
Span::raw(format!("{FIELD_INDENT}Resets:")),
|
||||
Span::raw(" "),
|
||||
Span::raw("at "),
|
||||
Span::raw(format!("{}", timing.next_reset_local)),
|
||||
Span::styled(
|
||||
format!(" (in {remaining})"),
|
||||
Style::default().fg(colors::dim()),
|
||||
),
|
||||
]);
|
||||
let mut spans: Vec<Span<'static>> = Vec::new();
|
||||
spans.push(Span::raw(prefix.clone()));
|
||||
spans.push(Span::raw("at "));
|
||||
spans.push(Span::raw(timing.next_reset_local));
|
||||
spans.push(Span::styled(
|
||||
format!(" (in {remaining})"),
|
||||
Style::default().fg(colors::dim()),
|
||||
));
|
||||
return Line::from(spans);
|
||||
}
|
||||
return Line::from(vec![Span::raw(format!(
|
||||
"{FIELD_INDENT}Reset timing updating…"
|
||||
))]);
|
||||
return Line::from(vec![
|
||||
Span::raw(prefix),
|
||||
Span::styled(
|
||||
"timing updating…".to_string(),
|
||||
Style::default().fg(colors::dim()),
|
||||
),
|
||||
]);
|
||||
}
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{FIELD_INDENT}Resets:")),
|
||||
Span::raw(" "),
|
||||
Span::styled(
|
||||
"shown once next window detected".to_string(),
|
||||
Style::default().fg(colors::dim()),
|
||||
),
|
||||
])
|
||||
let mut spans: Vec<Span<'static>> = Vec::new();
|
||||
spans.push(Span::raw(field_prefix("Resets")));
|
||||
spans.push(Span::styled(
|
||||
"shown once next window detected".to_string(),
|
||||
Style::default().fg(colors::dim()),
|
||||
));
|
||||
Line::from(spans)
|
||||
}
|
||||
|
||||
fn build_compact_lines(reset_info: &RateLimitResetInfo) -> Vec<Line<'static>> {
|
||||
@@ -373,7 +416,7 @@ fn build_compact_lines(reset_info: &RateLimitResetInfo) -> Vec<Line<'static>> {
|
||||
lines.push(build_compact_status_line(used, limit));
|
||||
} else {
|
||||
lines.push(Line::from(vec![Span::styled(
|
||||
format!("{FIELD_INDENT}Session usage updating…"),
|
||||
label_text("Session usage updating…"),
|
||||
Style::default().fg(colors::dim()),
|
||||
)]));
|
||||
}
|
||||
@@ -390,12 +433,12 @@ fn build_compact_lines(reset_info: &RateLimitResetInfo) -> Vec<Line<'static>> {
|
||||
));
|
||||
} else if reset_info.overflow_auto_compact {
|
||||
lines.push(Line::from(vec![Span::styled(
|
||||
format!("{FIELD_INDENT}Auto-compaction runs after overflow errors"),
|
||||
label_text("Auto-compaction runs after overflow errors"),
|
||||
Style::default().fg(colors::dim()),
|
||||
)]));
|
||||
} else {
|
||||
lines.push(Line::from(vec![Span::styled(
|
||||
format!("{FIELD_INDENT}Auto-compaction unavailable"),
|
||||
label_text("Auto-compaction unavailable"),
|
||||
Style::default().fg(colors::dim()),
|
||||
)]));
|
||||
}
|
||||
@@ -597,7 +640,6 @@ fn build_status_line(metrics: &RateLimitMetrics) -> Line<'static> {
|
||||
(false, false) => unreachable!(),
|
||||
};
|
||||
Line::from(vec![
|
||||
Span::raw(SECTION_INDENT),
|
||||
Span::styled(
|
||||
format!("✕ Rate limited: {reason}"),
|
||||
Style::default().fg(colors::error()),
|
||||
@@ -605,7 +647,6 @@ fn build_status_line(metrics: &RateLimitMetrics) -> Line<'static> {
|
||||
])
|
||||
} else {
|
||||
Line::from(vec![
|
||||
Span::raw(SECTION_INDENT),
|
||||
Span::styled(
|
||||
"✓ Within current limits".to_string(),
|
||||
Style::default().fg(colors::success()),
|
||||
@@ -620,9 +661,10 @@ fn build_legend_lines(show_gauge: bool) -> Vec<Line<'static>> {
|
||||
}
|
||||
|
||||
let mut lines = Vec::new();
|
||||
let indent = chart_indent();
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(
|
||||
CHART_INDENT.to_string(),
|
||||
indent.clone(),
|
||||
Style::default().fg(colors::dim()),
|
||||
),
|
||||
Span::styled(
|
||||
@@ -638,7 +680,7 @@ fn build_legend_lines(show_gauge: bool) -> Vec<Line<'static>> {
|
||||
]));
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(
|
||||
CHART_INDENT.to_string(),
|
||||
indent.clone(),
|
||||
Style::default().fg(colors::dim()),
|
||||
),
|
||||
Span::styled(
|
||||
@@ -654,7 +696,7 @@ fn build_legend_lines(show_gauge: bool) -> Vec<Line<'static>> {
|
||||
]));
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(
|
||||
CHART_INDENT.to_string(),
|
||||
indent,
|
||||
Style::default().fg(colors::dim()),
|
||||
),
|
||||
Span::styled(
|
||||
@@ -672,10 +714,17 @@ fn build_legend_lines(show_gauge: bool) -> Vec<Line<'static>> {
|
||||
}
|
||||
|
||||
fn field_prefix(label: &str) -> String {
|
||||
let target_width = 7usize;
|
||||
let padding = 2 + target_width.saturating_sub(label.len());
|
||||
let padding = INDENTS
|
||||
.label_gap
|
||||
.saturating_add(INDENTS.label_target_width.saturating_sub(label.len()));
|
||||
let spaces = " ".repeat(padding);
|
||||
format!("{FIELD_INDENT}{label}:{spaces}")
|
||||
let indent = label_indent();
|
||||
let mut text = String::with_capacity(indent.len() + label.len() + 1 + spaces.len());
|
||||
text.push_str(&indent);
|
||||
text.push_str(label);
|
||||
text.push(':');
|
||||
text.push_str(&spaces);
|
||||
text
|
||||
}
|
||||
|
||||
fn render_percent_bar(percent: f64) -> Vec<Span<'static>> {
|
||||
@@ -813,7 +862,7 @@ impl GridLayout {
|
||||
}
|
||||
let cell_width = WEEKLY_CELL.chars().count();
|
||||
|
||||
let indent_width = CHART_INDENT.chars().count() as u16;
|
||||
let indent_width = chart_indent_width() as u16;
|
||||
let available_inner = width.saturating_sub(indent_width) as usize;
|
||||
if available_inner < cell_width {
|
||||
return None;
|
||||
@@ -846,9 +895,10 @@ impl GridLayout {
|
||||
let counts = self.cell_counts(state);
|
||||
let mut lines = Vec::new();
|
||||
let mut cell_index = 0isize;
|
||||
let indent = chart_indent();
|
||||
for _ in 0..self.size {
|
||||
let mut spans: Vec<Span<'static>> = Vec::new();
|
||||
spans.push(Span::raw(CHART_INDENT));
|
||||
spans.push(Span::raw(indent.clone()));
|
||||
|
||||
for col in 0..self.size {
|
||||
if col > 0 {
|
||||
|
||||
+12
-8
@@ -1,6 +1,6 @@
|
||||
# Using Code with Zed via ACP
|
||||
|
||||
The Rust MCP server now exposes the Agent Client Protocol (ACP) tools (`acp/new_session`, `acp/prompt`) that Zed expects. Zed still connects over MCP/JSON-RPC, but every conversation is represented through these ACP calls. This section walks through a minimal setup.
|
||||
The Rust MCP server now exposes the Agent Client Protocol (ACP) primitives (`session/new`, `session/prompt`, plus streaming `session/update` notifications) that Zed expects. Zed still connects over MCP/JSON-RPC, but every conversation is represented through these ACP calls. This section walks through a minimal setup.
|
||||
|
||||
## 1. Configure Code's MCP server
|
||||
|
||||
@@ -22,19 +22,23 @@ Any existing MCP servers can remain in this table; the exiting overrides simply
|
||||
|
||||
## 2. Launch the MCP server under the new name
|
||||
|
||||
If you globally installed Code from npm (`npm install -g @just-every/code`), launch the MCP server with the built-in subcommand (`code mcp`; `code acp` is an equivalent alias and works well when `code` already points to VS Code):
|
||||
If you prefer a one-off launch without installing anything globally, run the MCP server via `npx`:
|
||||
|
||||
```bash
|
||||
code mcp
|
||||
npx -y @just-every/code acp
|
||||
# or pin to the latest dist-tag explicitly
|
||||
npx -y @just-every/code@latest acp
|
||||
```
|
||||
|
||||
Want a globally available binary instead? Install once (`npm install -g @just-every/code`) and then use the subcommand aliases (`code mcp`, `code acp`, or `coder acp`).
|
||||
|
||||
Prefer building from source? The previous workflow still works:
|
||||
|
||||
```bash
|
||||
cargo run -p code-mcp-server -- --stdio
|
||||
```
|
||||
|
||||
The server will advertise four tools during the handshake: `codex`, `codex-reply`, `acp/new_session`, and `acp/prompt`.
|
||||
The server will advertise four tools during the handshake: `codex`, `codex-reply`, `session/new`, and `session/prompt`.
|
||||
|
||||
## 3. Point Zed at Code's MCP endpoint
|
||||
|
||||
@@ -44,16 +48,16 @@ Add an entry to Zed's `settings.json` under `agent_servers` (see [Zed’s extern
|
||||
{
|
||||
"agent_servers": {
|
||||
"Code": {
|
||||
"command": "coder",
|
||||
"args": ["acp"]
|
||||
"command": "npx",
|
||||
"args": ["-y", "@just-every/code", "acp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can swap `coder` for an absolute path or `code` depending on your PATH preferences. Environment overrides such as `CODEX_HOME` or `RUST_LOG` are optional—set them only if you need a custom config directory or debug logging.
|
||||
Pinning explicitly to the latest dist-tag works as well: replace "@just-every/code" with "@just-every/code@latest" in the `args` array. If you already have the CLI installed globally, swap in "coder" (or any absolute path) for the command and pass ["acp"] as the arguments. Environment overrides such as `CODEX_HOME` or `RUST_LOG` are optional—set them only if you need a custom config directory or debug logging.
|
||||
|
||||
When Zed launches this server it connects over MCP, then issues ACP tool calls (`acp/new_session`, `acp/prompt`) that we expose. Those tool invocations are bridged into full Codex sessions, and we stream ACP `session_update` notifications back so Zed can render reasoning, tool executions, and approvals.
|
||||
When Zed launches this server it connects over MCP, then issues ACP tool calls (`session/new`, `session/prompt`) that we expose. Those tool invocations are bridged into full Codex sessions, and we stream ACP `session/update` notifications back so Zed can render reasoning, tool executions, and approvals. Zed can also send `session/cancel` to interrupt a running turn, which the server now honors by propagating an interrupt to Codex and replying with `stopReason: "cancelled"`.
|
||||
|
||||
## 4. Permission flow summary
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
## Context
|
||||
|
||||
Upstream PR [openai/codex#1707](https://github.com/openai/codex/pull/1707) introduces an
|
||||
experimental Agent Client Protocol (ACP) bridge so Zed can drive Codex via new
|
||||
`acp/new_session` and `acp/prompt` MCP tools. The branch (fetched locally as
|
||||
experimental Agent Client Protocol (ACP) bridge so Zed can drive Codex via the
|
||||
`session/new` and `session/prompt` tool calls. The branch (fetched locally as
|
||||
`upstream-pr-1707`) rewrites large portions of `codex-rs/core`, the MCP server,
|
||||
and the TypeScript CLI/TUI to accommodate the new workflow.
|
||||
|
||||
@@ -13,9 +13,9 @@ and the TypeScript CLI/TUI to accommodate the new workflow.
|
||||
- **Core** – Apply-patch execution now runs in-process with the ACP filesystem
|
||||
shim, preserving Code’s validation harness and approval prompts while
|
||||
emitting ACP tool-call metadata for downstream consumers.
|
||||
- **MCP Server** – `acp/new_session` and `acp/prompt` are available alongside
|
||||
- **MCP Server** – `session/new` and `session/prompt` are available alongside
|
||||
existing Codex tools. The new `acp_tool_runner` bridges Codex events to ACP
|
||||
`SessionUpdate`s and reuses the existing conversation manager to track live
|
||||
`session/update` notifications and reuses the existing conversation manager to track live
|
||||
sessions.
|
||||
- **Configuration** – `ConfigOverrides` and the TOML schema understand
|
||||
`experimental_client_tools` plus inline MCP server definitions, allowing IDE
|
||||
@@ -43,8 +43,8 @@ capabilities.
|
||||
- **MCP server (`codex-rs/mcp-server`)**
|
||||
- New `acp_tool_runner.rs` that spawns Codex sessions on demand, relays MCP
|
||||
notifications, and surfaces ACP updates.
|
||||
- `message_processor.rs` extended to expose `acp/new_session` and
|
||||
`acp/prompt`, and to translate Codex events into ACP notifications.
|
||||
- `message_processor.rs` extended to expose `session/new` and
|
||||
`session/prompt`, and to translate Codex events into ACP notifications (`session/update`).
|
||||
- **CLI/TUI (Node + Rust)**
|
||||
- TypeScript terminal UI completely replaced with ACP-first experience.
|
||||
- Rust TUI and associated tests removed.
|
||||
|
||||
Reference in New Issue
Block a user