diff --git a/Cargo.lock b/Cargo.lock index df093ae..2f6d01f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -198,9 +198,9 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.12.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4361ba6627e51de955b10f3c77fb9eb959c85191a236c1c2c84e32f4ff240faf" +checksum = "5efba6592048ef8a9ac97de8d79b2d9933d8ac4d94f7a2de102348fed0c61103" dependencies = [ "agent-client-protocol-derive", "agent-client-protocol-schema", @@ -209,23 +209,20 @@ dependencies = [ "futures", "futures-concurrency", "jsonrpcmsg", - "rmcp", "rustc-hash", "schemars 1.2.1", "serde", "serde_json", "shell-words", - "tokio", - "tokio-util", "tracing", "uuid", ] [[package]] name = "agent-client-protocol-derive" -version = "0.11.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cabdc9d845d08ec7ed2d0c9de1ae4a1b198301407d55855261572761be90ec9f" +checksum = "4d176a10d4cb06e0262a738c3c5bf21ff0968db13a666e31cbca94a3d3d72e7c" dependencies = [ "quote", "syn 2.0.117", @@ -233,9 +230,9 @@ dependencies = [ [[package]] name = "agent-client-protocol-schema" -version = "0.13.2" +version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b957d8391ac3933e2a940446171c508d2b8ffc386d8fa7d0b9c936a2575b463e" +checksum = "c290bfa00c6b52339db66f8e9cf711d5f08530800529f7d619ff24d6cba253d0" dependencies = [ "anyhow", "derive_more 2.1.1", @@ -1995,7 +1992,7 @@ dependencies = [ "tracing", "url", "uuid", - "which 8.0.2", + "which 8.0.3", "whoami 1.6.1", ] @@ -2608,7 +2605,7 @@ dependencies = [ "tracing", "urlencoding", "webbrowser", - "which 8.0.2", + "which 8.0.3", ] [[package]] @@ -2666,7 +2663,7 @@ dependencies = [ "serde_json", "tracing", "url", - "which 8.0.2", + "which 8.0.3", ] [[package]] @@ -2685,7 +2682,7 @@ dependencies = [ "tree-sitter", "tree-sitter-bash", "url", - "which 8.0.2", + "which 8.0.3", ] [[package]] @@ -7845,9 +7842,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" dependencies = [ "bytes", "prost-derive", @@ -7855,9 +7852,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", "itertools 0.14.0", @@ -11331,9 +11328,9 @@ dependencies = [ [[package]] name = "which" -version = "8.0.2" +version = "8.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" +checksum = "c789537cf2f7f55be8e6192f92e464174ee55f91af622777f7f1ceb0dbccd03e" dependencies = [ "libc", ] diff --git a/Cargo.toml b/Cargo.toml index 4368123..16c12eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ name = "codex_acp" path = "src/lib.rs" [dependencies] -agent-client-protocol = { version = "=0.12.1", features = ["unstable"] } +agent-client-protocol = { version = "=0.14.0", features = ["unstable"] } anyhow = "1" clap = "4" codex-apply-patch = { git = "https://github.com/openai/codex", tag = "rust-v0.137.0" } diff --git a/src/codex_agent.rs b/src/codex_agent.rs index d1e096a..ed1d2c2 100644 --- a/src/codex_agent.rs +++ b/src/codex_agent.rs @@ -6,9 +6,10 @@ use acp::schema::{ LoadSessionRequest, LoadSessionResponse, LogoutCapabilities, LogoutRequest, LogoutResponse, McpCapabilities, McpServer, McpServerHttp, McpServerStdio, NewSessionRequest, NewSessionResponse, PromptCapabilities, PromptRequest, PromptResponse, ProtocolVersion, - SessionCapabilities, SessionCloseCapabilities, SessionId, SessionInfo, SessionListCapabilities, + ResumeSessionRequest, ResumeSessionResponse, SessionCapabilities, SessionCloseCapabilities, + SessionId, SessionInfo, SessionListCapabilities, SessionResumeCapabilities, SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, SetSessionModeRequest, - SetSessionModeResponse, SetSessionModelRequest, SetSessionModelResponse, + SetSessionModeResponse, }; use acp::{Agent, Client, ConnectTo, ConnectionTo, Error}; use agent_client_protocol as acp; @@ -187,6 +188,24 @@ impl CodexAgent { }, acp::on_receive_request!(), ) + .on_receive_request( + { + let agent = agent.clone(); + async move |request: ResumeSessionRequest, + responder, + cx: ConnectionTo| { + let agent = agent.clone(); + let session_cx = cx.clone(); + cx.spawn(async move { + responder.respond_with_result( + agent.resume_session(request, session_cx).await, + ) + })?; + Ok(()) + } + }, + acp::on_receive_request!(), + ) .on_receive_request( { let agent = agent.clone(); @@ -261,21 +280,6 @@ impl CodexAgent { }, acp::on_receive_request!(), ) - .on_receive_request( - { - let agent = agent.clone(); - async move |request: SetSessionModelRequest, - responder, - cx: ConnectionTo| { - let agent = agent.clone(); - cx.spawn(async move { - responder.respond_with_result(agent.set_session_model(request).await) - })?; - Ok(()) - } - }, - acp::on_receive_request!(), - ) .on_receive_request( { let agent = agent.clone(); @@ -447,7 +451,8 @@ impl CodexAgent { agent_capabilities.session_capabilities = SessionCapabilities::new() .close(SessionCloseCapabilities::new()) - .list(SessionListCapabilities::new()); + .list(SessionListCapabilities::new()) + .resume(SessionResumeCapabilities::new()); let mut auth_methods = vec![ CodexAuthMethod::ChatGpt.into(), @@ -590,7 +595,6 @@ impl CodexAgent { Ok(NewSessionResponse::new(session_id) .modes(load.modes) - .models(load.models) .config_options(load.config_options)) } @@ -610,6 +614,43 @@ impl CodexAgent { .. } = request; + self.restore_session(session_id, cwd, mcp_servers, cx, true) + .await + } + + async fn resume_session( + &self, + request: ResumeSessionRequest, + cx: ConnectionTo, + ) -> Result { + info!("Resuming session: {}", request.session_id); + // Check before sending if authentication was successful or not + self.check_auth().await?; + + let ResumeSessionRequest { + session_id, + cwd, + mcp_servers, + .. + } = request; + + let load = self + .restore_session(session_id, cwd, mcp_servers, cx, false) + .await?; + + Ok(ResumeSessionResponse::new() + .modes(load.modes) + .config_options(load.config_options)) + } + + async fn restore_session( + &self, + session_id: SessionId, + cwd: PathBuf, + mcp_servers: Vec, + cx: ConnectionTo, + replay_history: bool, + ) -> Result { let rollout_path = find_thread_path_by_id_str( &self.config.codex_home, session_id.0.as_ref(), @@ -619,14 +660,18 @@ impl CodexAgent { .map_err(|e| Error::internal_error().data(e.to_string()))? .ok_or_else(|| Error::resource_not_found(None))?; - let history = RolloutRecorder::get_rollout_history(&rollout_path) - .await - .map_err(|e| Error::internal_error().data(e.to_string()))?; + let rollout_items = if replay_history { + let history = RolloutRecorder::get_rollout_history(&rollout_path) + .await + .map_err(|e| Error::internal_error().data(e.to_string()))?; - let rollout_items = match &history { - InitialHistory::Resumed(resumed) => resumed.history.clone(), - InitialHistory::Forked(items) => items.clone(), - InitialHistory::Cleared | InitialHistory::New => Vec::new(), + match &history { + InitialHistory::Resumed(resumed) => resumed.history.clone(), + InitialHistory::Forked(items) => items.clone(), + InitialHistory::Cleared | InitialHistory::New => Vec::new(), + } + } else { + Vec::new() }; let config = self.build_session_config(&cwd, mcp_servers)?; @@ -654,7 +699,9 @@ impl CodexAgent { cx, )); - thread.replay_history(rollout_items).await?; + if replay_history { + thread.replay_history(rollout_items).await?; + } let load = thread.load().await?; @@ -666,7 +713,6 @@ impl CodexAgent { Ok(LoadSessionResponse::new() .modes(load.modes) - .models(load.models) .config_options(load.config_options)) } @@ -783,19 +829,6 @@ impl CodexAgent { Ok(SetSessionModeResponse::default()) } - async fn set_session_model( - &self, - args: SetSessionModelRequest, - ) -> Result { - info!("Setting session model for session: {}", args.session_id); - - self.get_thread(&args.session_id)? - .set_model(args.model_id) - .await?; - - Ok(SetSessionModelResponse::default()) - } - async fn set_session_config_option( &self, args: SetSessionConfigOptionRequest, diff --git a/src/thread.rs b/src/thread.rs index da8e8db..fd1f1c2 100644 --- a/src/thread.rs +++ b/src/thread.rs @@ -12,16 +12,16 @@ use agent_client_protocol::{ schema::{ AvailableCommand, AvailableCommandInput, AvailableCommandsUpdate, ClientCapabilities, ConfigOptionUpdate, Content, ContentBlock, ContentChunk, Diff, EmbeddedResource, - EmbeddedResourceResource, ImageContent, LoadSessionResponse, Meta, ModelId, ModelInfo, - PermissionOption, PermissionOptionKind, Plan, PlanEntry, PlanEntryPriority, - PlanEntryStatus, PromptRequest, RequestPermissionOutcome, RequestPermissionRequest, - RequestPermissionResponse, ResourceLink, SelectedPermissionOutcome, SessionConfigId, - SessionConfigOption, SessionConfigOptionCategory, SessionConfigOptionValue, - SessionConfigSelectOption, SessionConfigValueId, SessionId, SessionMode, SessionModeId, - SessionModeState, SessionModelState, SessionNotification, SessionUpdate, StopReason, - Terminal, TextContent, TextResourceContents, ToolCall, ToolCallContent, ToolCallId, - ToolCallLocation, ToolCallStatus, ToolCallUpdate, ToolCallUpdateFields, ToolKind, - UnstructuredCommandInput, UsageUpdate, + EmbeddedResourceResource, ImageContent, LoadSessionResponse, Meta, PermissionOption, + PermissionOptionKind, Plan, PlanEntry, PlanEntryPriority, PlanEntryStatus, PromptRequest, + RequestPermissionOutcome, RequestPermissionRequest, RequestPermissionResponse, + ResourceLink, SelectedPermissionOutcome, SessionConfigId, SessionConfigOption, + SessionConfigOptionCategory, SessionConfigOptionValue, SessionConfigSelectOption, + SessionConfigValueId, SessionId, SessionMode, SessionModeId, SessionModeState, + SessionNotification, SessionUpdate, StopReason, Terminal, TextContent, + TextResourceContents, ToolCall, ToolCallContent, ToolCallId, ToolCallLocation, + ToolCallStatus, ToolCallUpdate, ToolCallUpdateFields, ToolKind, UnstructuredCommandInput, + UsageUpdate, }, }; use codex_apply_patch::parse_patch; @@ -284,10 +284,6 @@ enum ThreadMessage { mode: SessionModeId, response_tx: oneshot::Sender>, }, - SetModel { - model: ModelId, - response_tx: oneshot::Sender>, - }, SetConfigOption { config_id: SessionConfigId, value: SessionConfigOptionValue, @@ -401,17 +397,6 @@ impl Thread { .map_err(|e| Error::internal_error().data(e.to_string()))? } - pub async fn set_model(&self, model: ModelId) -> Result<(), Error> { - let (response_tx, response_rx) = oneshot::channel(); - - let message = ThreadMessage::SetModel { model, response_tx }; - drop(self.message_tx.send(message)); - - response_rx - .await - .map_err(|e| Error::internal_error().data(e.to_string()))? - } - pub async fn set_config_option( &self, config_id: SessionConfigId, @@ -2863,11 +2848,6 @@ impl ThreadActor { drop(response_tx.send(result)); self.maybe_emit_config_options_update().await; } - ThreadMessage::SetModel { model, response_tx } => { - let result = self.handle_set_model(model).await; - drop(response_tx.send(result)); - self.maybe_emit_config_options_update().await; - } ThreadMessage::SetConfigOption { config_id, value, @@ -2967,37 +2947,6 @@ impl ThreadActor { )) } - async fn find_current_model(&self) -> Option { - let model_presets = self.models_manager.list_models().await; - let config_model = self.get_current_model().await; - let preset = model_presets - .iter() - .find(|preset| preset.model == config_model)?; - - let effort = self - .config - .model_reasoning_effort - .and_then(|effort| { - preset - .supported_reasoning_efforts - .iter() - .find_map(|e| (e.effort == effort).then_some(effort)) - }) - .unwrap_or(preset.default_reasoning_effort); - - Some(Self::model_id(&preset.id, effort)) - } - - fn model_id(id: &str, effort: ReasoningEffort) -> ModelId { - ModelId::new(format!("{id}/{effort}")) - } - - fn parse_model_id(id: &ModelId) -> Option<(String, ReasoningEffort)> { - let (model, reasoning) = id.0.split_once('/')?; - let reasoning = serde_json::from_value(reasoning.into()).ok()?; - Some((model.to_owned(), reasoning)) - } - async fn config_options(&self) -> Result, Error> { let mut options = Vec::new(); @@ -3215,42 +3164,8 @@ impl ThreadActor { Ok(()) } - async fn models(&self) -> Result { - let mut available_models = Vec::new(); - let config_model = self.get_current_model().await; - - let current_model_id = if let Some(model_id) = self.find_current_model().await { - model_id - } else { - // If no preset found, return the current model string as-is - let model_id = ModelId::new(self.get_current_model().await); - available_models.push(ModelInfo::new(model_id.clone(), model_id.to_string())); - model_id - }; - - available_models.extend( - self.models_manager - .list_models() - .await - .iter() - .filter(|model| model.show_in_picker || model.model == config_model) - .flat_map(|preset| { - preset.supported_reasoning_efforts.iter().map(|effort| { - ModelInfo::new( - Self::model_id(&preset.id, effort.effort), - format!("{} ({})", preset.display_name, effort.effort), - ) - .description(format!("{} {}", preset.description, effort.description)) - }) - }), - ); - - Ok(SessionModelState::new(current_model_id, available_models)) - } - async fn handle_load(&mut self) -> Result { Ok(LoadSessionResponse::new() - .models(self.models().await?) .modes(self.modes()) .config_options(self.config_options().await?)) } @@ -3410,41 +3325,6 @@ impl ThreadActor { self.models_manager.get_model(&self.config.model).await } - async fn handle_set_model(&mut self, model: ModelId) -> Result<(), Error> { - // Try parsing as preset format, otherwise use as-is, fallback to config - let (model_to_use, effort_to_use) = if let Some((m, e)) = Self::parse_model_id(&model) { - (m, Some(e)) - } else { - let model_str = model.0.to_string(); - let fallback = if !model_str.is_empty() { - model_str - } else { - self.get_current_model().await - }; - (fallback, self.config.model_reasoning_effort) - }; - - if model_to_use.is_empty() { - return Err(Error::invalid_params().data("No model parsed or configured")); - } - - self.thread - .submit(Op::ThreadSettings { - thread_settings: ThreadSettingsOverrides { - model: Some(model_to_use.clone()), - effort: Some(effort_to_use), - ..Default::default() - }, - }) - .await - .map_err(|e| Error::from(anyhow::anyhow!(e)))?; - - self.config.model = Some(model_to_use); - self.config.model_reasoning_effort = effort_to_use; - - Ok(()) - } - async fn handle_cancel(&mut self) -> Result<(), Error> { self.detach_pending_interactions(); self.thread