mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
fix(mcp-server/acp): expose model selection to Zed
This commit is contained in:
@@ -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.4.0"
|
||||
agent-client-protocol = "0.4.3"
|
||||
dirs = "6"
|
||||
env-flags = "0.1.1"
|
||||
eventsource-stream = "0.2.3"
|
||||
|
||||
@@ -16,7 +16,7 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
agent-client-protocol = "0.4.2"
|
||||
agent-client-protocol = { version = "0.4.3", features = ["unstable"] }
|
||||
codex-arg0 = { workspace = true }
|
||||
codex-common = { workspace = true, features = ["cli"] }
|
||||
codex-core = { workspace = true }
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Asynchronous worker that executes an ACP tool-call inside a spawned Tokio task.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_client_protocol as acp;
|
||||
@@ -18,20 +17,21 @@ use mcp_types::CallToolResult;
|
||||
use mcp_types::ContentBlock;
|
||||
use mcp_types::RequestId;
|
||||
use mcp_types::TextContent;
|
||||
use tokio::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::outgoing_message::OutgoingMessageSender;
|
||||
use crate::outgoing_message::OutgoingNotification;
|
||||
use crate::session_store::{SessionEntry, SessionMap};
|
||||
use serde_json;
|
||||
|
||||
pub async fn new_session(
|
||||
request_id: RequestId,
|
||||
config: CodexConfig,
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
session_map: Arc<Mutex<HashMap<Uuid, Arc<CodexConversation>>>>,
|
||||
session_map: SessionMap,
|
||||
conversation_manager: Arc<ConversationManager>,
|
||||
) -> Option<Uuid> {
|
||||
let config_for_session = config.clone();
|
||||
let NewConversation {
|
||||
conversation_id,
|
||||
conversation,
|
||||
@@ -54,12 +54,9 @@ pub async fn new_session(
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let session_uuid: Uuid = conversation_id.into();
|
||||
session_map
|
||||
.lock()
|
||||
.await
|
||||
.insert(session_uuid, conversation);
|
||||
let entry = SessionEntry::new(conversation.clone(), config_for_session);
|
||||
session_map.lock().await.insert(session_uuid, entry);
|
||||
|
||||
Some(session_uuid)
|
||||
}
|
||||
|
||||
@@ -183,6 +183,27 @@ pub(crate) fn create_tool_for_acp_prompt() -> Tool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a `Tool` definition for the `acp/set_model` tool-call.
|
||||
pub(crate) fn create_tool_for_acp_set_model() -> Tool {
|
||||
let input_schema = ToolInputSchema {
|
||||
r#type: "object".to_string(),
|
||||
required: Some(vec!["sessionId".to_string(), "modelId".to_string()]),
|
||||
properties: Some(json!({
|
||||
"sessionId": {"type": "string"},
|
||||
"modelId": {"type": "string"}
|
||||
})),
|
||||
};
|
||||
|
||||
Tool {
|
||||
name: acp::AGENT_METHOD_NAMES.model_select.to_string(),
|
||||
title: Some(acp::AGENT_METHOD_NAMES.model_select.to_string()),
|
||||
input_schema,
|
||||
output_schema: None,
|
||||
description: Some("Select a model for an existing ACP Codex session.".to_string()),
|
||||
annotations: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AcpNewSessionToolArgs {
|
||||
@@ -202,6 +223,15 @@ pub struct AcpPromptToolArgs {
|
||||
pub meta: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AcpSetModelToolArgs {
|
||||
#[serde(rename = "sessionId")]
|
||||
pub session_id: acp::SessionId,
|
||||
#[serde(rename = "modelId")]
|
||||
pub model_id: acp::ModelId,
|
||||
}
|
||||
|
||||
impl CodexToolCallParam {
|
||||
/// Returns the initial user prompt to start the Codex conversation and the
|
||||
/// effective Config object generated from the supplied parameters.
|
||||
|
||||
@@ -30,6 +30,7 @@ use crate::exec_approval::handle_exec_approval_request;
|
||||
use crate::outgoing_message::OutgoingMessageSender;
|
||||
use crate::outgoing_message::OutgoingNotificationMeta;
|
||||
use crate::patch_approval::handle_patch_approval_request;
|
||||
use crate::session_store::{SessionEntry, SessionMap};
|
||||
|
||||
pub(crate) const INVALID_PARAMS_ERROR_CODE: i64 = -32602;
|
||||
|
||||
@@ -42,10 +43,11 @@ pub async fn run_codex_tool_session(
|
||||
initial_prompt: String,
|
||||
config: CodexConfig,
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
session_map: Arc<Mutex<HashMap<Uuid, Arc<CodexConversation>>>>,
|
||||
session_map: SessionMap,
|
||||
conversation_manager: Arc<ConversationManager>,
|
||||
running_requests_id_to_codex_uuid: Arc<Mutex<HashMap<RequestId, Uuid>>>,
|
||||
) {
|
||||
let config_for_session = config.clone();
|
||||
let NewConversation {
|
||||
conversation_id,
|
||||
conversation,
|
||||
@@ -66,12 +68,9 @@ pub async fn run_codex_tool_session(
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let session_uuid: Uuid = conversation_id.into();
|
||||
session_map
|
||||
.lock()
|
||||
.await
|
||||
.insert(session_uuid, conversation.clone());
|
||||
let entry = SessionEntry::new(conversation.clone(), config_for_session);
|
||||
session_map.lock().await.insert(session_uuid, entry);
|
||||
|
||||
let session_configured_event = Event {
|
||||
// Use a fake id value for now.
|
||||
|
||||
@@ -30,6 +30,7 @@ mod json_to_toml;
|
||||
pub(crate) mod message_processor;
|
||||
mod outgoing_message;
|
||||
mod patch_approval;
|
||||
mod session_store;
|
||||
|
||||
use crate::message_processor::MessageProcessor;
|
||||
use crate::outgoing_message::OutgoingMessage;
|
||||
|
||||
@@ -4,29 +4,32 @@ use std::path::PathBuf;
|
||||
use crate::codex_message_processor::CodexMessageProcessor;
|
||||
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_acp_set_model;
|
||||
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::AcpSetModelToolArgs;
|
||||
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 crate::outgoing_message::{OutgoingMessageSender, OutgoingNotification};
|
||||
use crate::session_store::SessionMap;
|
||||
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;
|
||||
|
||||
use codex_common::model_presets::{builtin_model_presets, ModelPreset};
|
||||
use codex_core::AuthManager;
|
||||
use codex_core::ConversationManager;
|
||||
use codex_core::config_types::McpServerConfig;
|
||||
use codex_core::config_types::ClientTools;
|
||||
use codex_core::config_types::{ClientTools, McpServerConfig, ReasoningEffort};
|
||||
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::model_family::{derive_default_model_family, find_family_for_model};
|
||||
use codex_core::protocol::Submission;
|
||||
use codex_core::protocol::Op;
|
||||
use codex_protocol::mcp_protocol::AuthMode;
|
||||
@@ -56,7 +59,7 @@ pub(crate) struct MessageProcessor {
|
||||
initialized: bool,
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
conversation_manager: Arc<ConversationManager>,
|
||||
session_map: Arc<Mutex<HashMap<Uuid, Arc<CodexConversation>>>>,
|
||||
session_map: SessionMap,
|
||||
running_requests_id_to_codex_uuid: Arc<Mutex<HashMap<RequestId, Uuid>>>,
|
||||
base_config: Arc<Config>,
|
||||
}
|
||||
@@ -84,13 +87,14 @@ impl MessageProcessor {
|
||||
codex_linux_sandbox_exe.clone(),
|
||||
config_for_processor,
|
||||
);
|
||||
let session_map: SessionMap = Arc::new(Mutex::new(HashMap::new()));
|
||||
Self {
|
||||
codex_message_processor,
|
||||
outgoing,
|
||||
initialized: false,
|
||||
codex_linux_sandbox_exe,
|
||||
conversation_manager,
|
||||
session_map: Arc::new(Mutex::new(HashMap::new())),
|
||||
session_map,
|
||||
running_requests_id_to_codex_uuid: Arc::new(Mutex::new(HashMap::new())),
|
||||
base_config: config,
|
||||
}
|
||||
@@ -170,6 +174,35 @@ impl MessageProcessor {
|
||||
return;
|
||||
}
|
||||
|
||||
if request.method == acp::AGENT_METHOD_NAMES.model_select {
|
||||
tracing::info!("handling session/set_model via ACP shim");
|
||||
if let Some(params) = request.params.clone() {
|
||||
match serde_json::from_value::<acp::SetSessionModelRequest>(params) {
|
||||
Ok(model_params) => {
|
||||
self.handle_session_set_model(request_id, model_params)
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to parse session/set_model params: {err}");
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("invalid session/set_model params: {err}"),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: "session/set_model 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 {
|
||||
@@ -481,6 +514,7 @@ impl MessageProcessor {
|
||||
create_tool_for_codex_tool_call_reply_param(),
|
||||
create_tool_for_acp_new_session(),
|
||||
create_tool_for_acp_prompt(),
|
||||
create_tool_for_acp_set_model(),
|
||||
],
|
||||
next_cursor: None,
|
||||
};
|
||||
@@ -509,6 +543,9 @@ impl MessageProcessor {
|
||||
_ if name == acp::AGENT_METHOD_NAMES.session_prompt => {
|
||||
self.handle_tool_call_acp_prompt(id, arguments).await
|
||||
}
|
||||
_ if name == acp::AGENT_METHOD_NAMES.model_select => {
|
||||
self.handle_tool_call_acp_set_model(id, arguments).await
|
||||
}
|
||||
_ => {
|
||||
let result = CallToolResult {
|
||||
content: vec![ContentBlock::TextContent(TextContent {
|
||||
@@ -673,7 +710,8 @@ impl MessageProcessor {
|
||||
tokio::spawn(async move {
|
||||
let codex = {
|
||||
let map = session_map.lock().await;
|
||||
map.get(&session_id).cloned()
|
||||
map.get(&session_id)
|
||||
.map(|entry| entry.conversation.clone())
|
||||
};
|
||||
|
||||
let Some(codex) = codex else {
|
||||
@@ -746,8 +784,8 @@ impl MessageProcessor {
|
||||
tracing::info!("session_id: {session_id}");
|
||||
|
||||
// Obtain the Codex conversation from the session map, falling back to the conversation manager.
|
||||
let codex_arc = if let Some(conv) = self.session_map.lock().await.get(&session_id).cloned() {
|
||||
conv
|
||||
let codex_arc = if let Some(entry) = self.session_map.lock().await.get(&session_id) {
|
||||
entry.conversation.clone()
|
||||
} else {
|
||||
match self
|
||||
.conversation_manager
|
||||
@@ -823,6 +861,7 @@ impl MessageProcessor {
|
||||
let outgoing = self.outgoing.clone();
|
||||
let session_map = self.session_map.clone();
|
||||
let conversation_manager = self.conversation_manager.clone();
|
||||
let models_state = session_models_from_config(&config);
|
||||
|
||||
task::spawn(async move {
|
||||
let Some(session_id) = crate::acp_tool_runner::new_session(
|
||||
@@ -841,6 +880,7 @@ impl MessageProcessor {
|
||||
let response_struct = acp::NewSessionResponse {
|
||||
session_id: acp::SessionId(Arc::from(session_id_str.clone())),
|
||||
modes: Some(default_session_modes()),
|
||||
models: models_state.clone(),
|
||||
meta: None,
|
||||
};
|
||||
|
||||
@@ -889,6 +929,7 @@ impl MessageProcessor {
|
||||
let outgoing = self.outgoing.clone();
|
||||
let session_map = self.session_map.clone();
|
||||
let conversation_manager = self.conversation_manager.clone();
|
||||
let models_state = session_models_from_config(&config);
|
||||
|
||||
task::spawn(async move {
|
||||
let Some(session_id) = crate::acp_tool_runner::new_session(
|
||||
@@ -906,6 +947,7 @@ impl MessageProcessor {
|
||||
let response = acp::NewSessionResponse {
|
||||
session_id: acp::SessionId(Arc::from(session_id.to_string())),
|
||||
modes: Some(default_session_modes()),
|
||||
models: models_state.clone(),
|
||||
meta: None,
|
||||
};
|
||||
|
||||
@@ -959,12 +1001,12 @@ impl MessageProcessor {
|
||||
}
|
||||
};
|
||||
|
||||
let session = {
|
||||
let session_entry = {
|
||||
let map = self.session_map.lock().await;
|
||||
map.get(&session_uuid).cloned()
|
||||
};
|
||||
|
||||
let Some(session) = session else {
|
||||
let Some(session_entry) = session_entry else {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("unknown session id: {}", acp_session_id),
|
||||
@@ -974,6 +1016,8 @@ impl MessageProcessor {
|
||||
return;
|
||||
};
|
||||
|
||||
let session = session_entry.conversation.clone();
|
||||
|
||||
let outgoing = self.outgoing.clone();
|
||||
let requests_codex_map = self.running_requests_id_to_codex_uuid.clone();
|
||||
let prompt_blocks = params.prompt;
|
||||
@@ -1016,6 +1060,120 @@ impl MessageProcessor {
|
||||
});
|
||||
}
|
||||
|
||||
async fn handle_session_set_model(
|
||||
&self,
|
||||
request_id: RequestId,
|
||||
params: acp::SetSessionModelRequest,
|
||||
) {
|
||||
let session_string = params.session_id.to_string();
|
||||
let session_uuid = match Uuid::parse_str(&session_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;
|
||||
}
|
||||
};
|
||||
|
||||
match self
|
||||
.update_session_model_meta(session_uuid, params.session_id, params.model_id)
|
||||
.await
|
||||
{
|
||||
Ok(meta) => {
|
||||
let response = acp::SetSessionModelResponse { meta };
|
||||
self.outgoing.send_response(request_id, response).await;
|
||||
}
|
||||
Err(err) => {
|
||||
self.outgoing.send_error(request_id, err).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_session_model_meta(
|
||||
&self,
|
||||
session_uuid: Uuid,
|
||||
session_id: acp::SessionId,
|
||||
model_id: acp::ModelId,
|
||||
) -> Result<Option<serde_json::Value>, JSONRPCErrorError> {
|
||||
let entry = {
|
||||
let map = self.session_map.lock().await;
|
||||
map.get(&session_uuid).cloned()
|
||||
};
|
||||
|
||||
let Some(entry) = entry else {
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("unknown session id: {}", session_id),
|
||||
data: None,
|
||||
});
|
||||
};
|
||||
|
||||
let mut config_guard = entry.config.lock().await;
|
||||
let selection = match resolve_model_selection(&model_id, &config_guard) {
|
||||
Some(selection) => selection,
|
||||
None => {
|
||||
let message = format!("unknown model id: {}", model_id.to_string());
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message,
|
||||
data: None,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let changed = apply_model_selection(&mut config_guard, &selection.model, selection.effort);
|
||||
let models_state = session_models_from_config(&config_guard);
|
||||
let models_meta_value = models_state
|
||||
.as_ref()
|
||||
.and_then(|state| serde_json::to_value(state).ok());
|
||||
let response_meta = models_meta_value
|
||||
.clone()
|
||||
.map(|value| json!({ "models": value }));
|
||||
let configure_op = if changed {
|
||||
Some(configure_session_op_from_config(&config_guard))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let model_name = selection.model.clone();
|
||||
|
||||
drop(config_guard);
|
||||
|
||||
if let Some(op) = configure_op {
|
||||
if let Err(err) = entry.conversation.submit(op).await {
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: err.to_string(),
|
||||
data: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(models_meta) = models_meta_value {
|
||||
let notification = acp::SessionNotification {
|
||||
session_id: session_id.clone(),
|
||||
update: acp::SessionUpdate::AgentMessageChunk {
|
||||
content: acp::ContentBlock::from(format!("Model set to {model_name}")),
|
||||
},
|
||||
meta: Some(json!({ "models": models_meta })),
|
||||
};
|
||||
|
||||
if let Ok(params_value) = serde_json::to_value(notification) {
|
||||
self.outgoing
|
||||
.send_notification(OutgoingNotification {
|
||||
method: acp::CLIENT_METHOD_NAMES.session_update.to_string(),
|
||||
params: Some(params_value),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(response_meta)
|
||||
}
|
||||
|
||||
async fn handle_tool_call_acp_prompt(
|
||||
&self,
|
||||
request_id: RequestId,
|
||||
@@ -1041,7 +1199,8 @@ impl MessageProcessor {
|
||||
|
||||
let session = {
|
||||
let map = self.session_map.lock().await;
|
||||
map.get(&session_id).cloned()
|
||||
map.get(&session_id)
|
||||
.map(|entry| entry.conversation.clone())
|
||||
};
|
||||
|
||||
let Some(session) = session else {
|
||||
@@ -1101,6 +1260,80 @@ impl MessageProcessor {
|
||||
});
|
||||
}
|
||||
|
||||
async fn handle_tool_call_acp_set_model(
|
||||
&self,
|
||||
request_id: RequestId,
|
||||
arguments: Option<serde_json::Value>,
|
||||
) {
|
||||
let args = match arguments
|
||||
.context("arguments required")
|
||||
.and_then(|args| serde_json::from_value::<AcpSetModelToolArgs>(args).context("invalid arguments"))
|
||||
{
|
||||
Ok(args) => args,
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to parse arguments: {err}");
|
||||
let result = CallToolResult {
|
||||
content: vec![ContentBlock::TextContent(TextContent {
|
||||
r#type: "text".to_owned(),
|
||||
text: format!("Failed to parse arguments: {err}"),
|
||||
annotations: None,
|
||||
})],
|
||||
is_error: Some(true),
|
||||
structured_content: None,
|
||||
};
|
||||
self.outgoing.send_response(request_id, result).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let session_uuid = match Uuid::parse_str(&args.session_id.to_string()) {
|
||||
Ok(id) => id,
|
||||
Err(err) => {
|
||||
let result = CallToolResult {
|
||||
content: vec![ContentBlock::TextContent(TextContent {
|
||||
r#type: "text".to_owned(),
|
||||
text: format!("Invalid session id: {err}"),
|
||||
annotations: None,
|
||||
})],
|
||||
is_error: Some(true),
|
||||
structured_content: None,
|
||||
};
|
||||
self.outgoing.send_response(request_id, result).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match self
|
||||
.update_session_model_meta(session_uuid, args.session_id.clone(), args.model_id.clone())
|
||||
.await
|
||||
{
|
||||
Ok(meta) => {
|
||||
let structured = serde_json::to_value(acp::SetSessionModelResponse { meta })
|
||||
.unwrap_or_else(|_| json!({}));
|
||||
let result = CallToolResult {
|
||||
content: vec![],
|
||||
is_error: Some(false),
|
||||
structured_content: Some(structured),
|
||||
};
|
||||
self.outgoing.send_response(request_id, result).await;
|
||||
}
|
||||
Err(err) => {
|
||||
let result = CallToolResult {
|
||||
content: vec![ContentBlock::TextContent(TextContent {
|
||||
r#type: "text".to_owned(),
|
||||
text: err.message.clone(),
|
||||
annotations: None,
|
||||
})],
|
||||
is_error: Some(true),
|
||||
structured_content: None,
|
||||
};
|
||||
self.outgoing
|
||||
.send_response(request_id, result)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn acp_prompt_arguments(
|
||||
arguments: Option<serde_json::Value>,
|
||||
) -> anyhow::Result<(Uuid, acp::SessionId, Vec<acp::ContentBlock>)> {
|
||||
@@ -1136,7 +1369,8 @@ impl MessageProcessor {
|
||||
|
||||
let conversation = {
|
||||
let map = self.session_map.lock().await;
|
||||
map.get(&session_uuid).cloned()
|
||||
map.get(&session_uuid)
|
||||
.map(|entry| entry.conversation.clone())
|
||||
};
|
||||
|
||||
let Some(conversation) = conversation else {
|
||||
@@ -1238,6 +1472,154 @@ fn convert_mcp_servers(
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ModelSelection {
|
||||
model: String,
|
||||
effort: ReasoningEffort,
|
||||
}
|
||||
|
||||
fn session_models_from_config(config: &Config) -> Option<acp::SessionModelState> {
|
||||
let presets = builtin_model_presets(None);
|
||||
let mut available_models = Vec::new();
|
||||
let mut current_model_id: Option<acp::ModelId> = None;
|
||||
|
||||
for preset in presets.iter() {
|
||||
let id = acp::ModelId(Arc::from(preset.id));
|
||||
let description = if preset.description.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(preset.description.to_string())
|
||||
};
|
||||
available_models.push(acp::ModelInfo {
|
||||
model_id: id.clone(),
|
||||
name: preset.label.to_string(),
|
||||
description,
|
||||
meta: None,
|
||||
});
|
||||
|
||||
if preset.model.eq_ignore_ascii_case(&config.model) {
|
||||
let preset_effort = preset_effort(preset);
|
||||
if current_model_id.is_none() || preset_effort == config.model_reasoning_effort {
|
||||
current_model_id = Some(id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if available_models.is_empty() {
|
||||
let model_lower = config.model.to_ascii_lowercase();
|
||||
let id = acp::ModelId(Arc::from(format!("custom::{model_lower}")));
|
||||
available_models.push(acp::ModelInfo {
|
||||
model_id: id.clone(),
|
||||
name: config.model.clone(),
|
||||
description: Some("Configured via CODEX_HOME/config.toml".to_string()),
|
||||
meta: None,
|
||||
});
|
||||
return Some(acp::SessionModelState {
|
||||
current_model_id: id,
|
||||
available_models,
|
||||
meta: None,
|
||||
});
|
||||
}
|
||||
|
||||
if current_model_id.is_none() {
|
||||
let model_lower = config.model.to_ascii_lowercase();
|
||||
let id = acp::ModelId(Arc::from(format!("custom::{model_lower}")));
|
||||
available_models.push(acp::ModelInfo {
|
||||
model_id: id.clone(),
|
||||
name: config.model.clone(),
|
||||
description: Some("Configured via CODEX_HOME/config.toml".to_string()),
|
||||
meta: None,
|
||||
});
|
||||
current_model_id = Some(id);
|
||||
}
|
||||
|
||||
Some(acp::SessionModelState {
|
||||
current_model_id: current_model_id.expect("current model id set"),
|
||||
available_models,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn preset_effort(preset: &ModelPreset) -> ReasoningEffort {
|
||||
preset
|
||||
.effort
|
||||
.map(ReasoningEffort::from)
|
||||
.unwrap_or(ReasoningEffort::Medium)
|
||||
}
|
||||
|
||||
fn resolve_model_selection(model_id: &acp::ModelId, config: &Config) -> Option<ModelSelection> {
|
||||
let requested = model_id.to_string();
|
||||
let requested_lower = requested.to_ascii_lowercase();
|
||||
|
||||
for preset in builtin_model_presets(None).iter() {
|
||||
if preset.id.eq_ignore_ascii_case(&requested)
|
||||
|| preset.label.eq_ignore_ascii_case(&requested)
|
||||
|| preset.model.eq_ignore_ascii_case(&requested)
|
||||
{
|
||||
return Some(ModelSelection {
|
||||
model: preset.model.to_string(),
|
||||
effort: preset_effort(preset),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(stripped) = requested_lower.strip_prefix("custom::") {
|
||||
let effort = if stripped.eq_ignore_ascii_case(&config.model) {
|
||||
config.model_reasoning_effort
|
||||
} else {
|
||||
ReasoningEffort::Medium
|
||||
};
|
||||
return Some(ModelSelection {
|
||||
model: stripped.to_string(),
|
||||
effort,
|
||||
});
|
||||
}
|
||||
|
||||
if requested_lower == config.model.to_ascii_lowercase() {
|
||||
return Some(ModelSelection {
|
||||
model: config.model.clone(),
|
||||
effort: config.model_reasoning_effort,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn apply_model_selection(config: &mut Config, model: &str, effort: ReasoningEffort) -> bool {
|
||||
let mut updated = false;
|
||||
if !config.model.eq_ignore_ascii_case(model) {
|
||||
config.model = model.to_string();
|
||||
config.model_family = find_family_for_model(&config.model)
|
||||
.unwrap_or_else(|| derive_default_model_family(&config.model));
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if config.model_reasoning_effort != effort {
|
||||
config.model_reasoning_effort = effort;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
updated
|
||||
}
|
||||
|
||||
fn configure_session_op_from_config(config: &Config) -> Op {
|
||||
Op::ConfigureSession {
|
||||
provider: config.model_provider.clone(),
|
||||
model: config.model.clone(),
|
||||
model_reasoning_effort: config.model_reasoning_effort,
|
||||
model_reasoning_summary: config.model_reasoning_summary,
|
||||
model_text_verbosity: config.model_text_verbosity,
|
||||
user_instructions: config.user_instructions.clone(),
|
||||
base_instructions: config.base_instructions.clone(),
|
||||
approval_policy: config.approval_policy.clone(),
|
||||
sandbox_policy: config.sandbox_policy.clone(),
|
||||
disable_response_storage: config.disable_response_storage,
|
||||
notify: config.notify.clone(),
|
||||
cwd: config.cwd.clone(),
|
||||
resume_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn default_session_modes() -> acp::SessionModeState {
|
||||
let mode_id = acp::SessionModeId(Arc::from("default".to_string()));
|
||||
let mode = acp::SessionMode {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_core::config::Config;
|
||||
use codex_core::CodexConversation;
|
||||
use tokio::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// In-memory session entry tracking an active Codex conversation and its config.
|
||||
#[derive(Clone)]
|
||||
pub struct SessionEntry {
|
||||
pub conversation: Arc<CodexConversation>,
|
||||
pub config: Arc<Mutex<Config>>,
|
||||
}
|
||||
|
||||
impl SessionEntry {
|
||||
pub fn new(conversation: Arc<CodexConversation>, config: Config) -> Self {
|
||||
Self {
|
||||
conversation,
|
||||
config: Arc::new(Mutex::new(config)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type SessionMap = Arc<Mutex<HashMap<Uuid, SessionEntry>>>;
|
||||
@@ -26,6 +26,16 @@ Adjust the `command` or `args` only if you pin a different version or use a glob
|
||||
- The Rust MCP server exposes ACP tools: `session/new`, `session/prompt`, and fast interrupts via `session/cancel`. These are backed by the same conversation manager that powers the TUI, so approvals, confirm guards, and sandbox policies remain intact.
|
||||
- Streaming `session/update` notifications bridge Codex events into Zed. You get Answer/Reasoning updates, shell command progress, approvals, and apply_patch diffs in the Zed UI without losing terminal parity.
|
||||
- MCP configuration stays centralized in `CODEX_HOME/config.toml`. Use `[experimental_client_tools]` to delegate file read/write and permission requests back to Zed when you want its UI to handle approvals.
|
||||
- MCP configuration stays centralized in `CODEX_HOME/config.toml`. Use `[experimental_client_tools]` to delegate file read/write and permission requests back to Zed when you want its UI to handle approvals. A minimal setup looks like:
|
||||
|
||||
```toml
|
||||
[experimental_client_tools]
|
||||
request_permission = { mcp_server = "zed", tool_name = "requestPermission" }
|
||||
read_text_file = { mcp_server = "zed", tool_name = "readTextFile" }
|
||||
write_text_file = { mcp_server = "zed", tool_name = "writeTextFile" }
|
||||
```
|
||||
|
||||
Zed wires these tools automatically when you add the Code agent, so the identifiers above match the defaults.
|
||||
- The CLI entry point (`npx @just-every/code acp`) is a thin wrapper over the Rust binary (`cargo run -p code-mcp-server -- --stdio`) that ships alongside the rest of Code. Build-from-source workflows plug in by swapping `command` for an absolute path to that binary.
|
||||
|
||||
## Tips and troubleshooting
|
||||
@@ -33,3 +43,4 @@ Adjust the `command` or `args` only if you pin a different version or use a glob
|
||||
- Need to inspect the handshake? Run Zed’s `dev: open acp logs` command from the Command Palette; the log shows JSON‑RPC requests and Codex replies.
|
||||
- If prompts hang, make sure no other process is bound to the same MCP port and that your `CODEX_HOME` points to the intended config directory. The ACP server inherits all of Code’s sandbox settings, so restrictive policies (e.g., `approval_policy = "never"`) still apply.
|
||||
- Zed currently skips history restores and checkpoint UI for third-party agents. Stick to the TUI if you rely on those features; ACP support is still evolving upstream.
|
||||
- After a session starts, the model selector in Zed lists Code’s built-in presets (e.g., `gpt-5-codex`, `gpt-5` high/medium/low). Picking a new preset updates the running Codex session immediately, so you don’t have to restart the agent to change models.
|
||||
|
||||
Reference in New Issue
Block a user