diff --git a/CHANGELOG.md b/CHANGELOG.md index f3a6080ec..8c7be2032 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,15 +21,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - `refactor(llm)`: `OpenAiProvider::generation_params()` private helper extracted; removes 4 duplicate `generation_overrides` tuple expansions in `send_request`, `send_stream_request`, `debug_request_json`, and `chat_with_tools`. (#4873) +- `build(acp)`: bump `agent-client-protocol` 0.12.1 → 0.14.0 and `agent-client-protocol-schema` = 0.13.6; remove dead `agent-client-protocol-tokio` dependency +- `feat(acp)`: `unstable-session-usage` feature now maps to upstream `unstable_end_turn_token_usage` gate (renamed, not stabilized in 0.14.0) +- `feat(acp)`: `unstable-elicitation` feature now also enables `agent-client-protocol/unstable_elicitation` core passthrough for `elicitation/create` handler wiring +- `refactor(acp)`: provider ext-method types renamed to singular: `SetProvidersRequest/Response` → `SetProviderRequest/Response`, `DisableProvidersRequest/Response` → `DisableProviderRequest/Response` +- `refactor(acp)`: logout, session/close, session/delete, session/resume, and additional-directories handlers are now unconditional (feature flags retained as no-op tombstones for workspace forwarding compatibility) + +### Removed + +- `refactor(acp)`: `session/set_model` dedicated RPC method removed (deleted upstream in 0.14.0); model switching remains fully available via `session/set_config_option` (config_id="model") and the `$/model` slash command +- `refactor(acp)`: inbound/outbound message-id echo removed (`PromptRequest.message_id` and `PromptResponse.user_message_id` were removed upstream in 0.14.0) + +### Fixed + - `refactor(llm)`: `parse_gemini_error` in `zeph-llm` now delegates the base `ApiError` and `ContextLengthExceeded` construction to `crate::http::map_error_response`, consistent with all other backends (claude, openai, gonka, cocoon). Gemini-specific `RESOURCE_EXHAUSTED → RateLimited` handling is preserved. (#4868) - refactor(memory): remove `ScoredCandidate` single-field wrapper in `tiered_retrieval.rs` ([#4867](https://github.com/bug-ops/zeph/issues/4867)) - fix(memory): replace `.entered()` span guard with removal in `tiered_retrieval.rs` async fn ([#4866](https://github.com/bug-ops/zeph/issues/4866)) - -### Fixed - - `docs(config)`: `CandleConfig` in `zeph-config` now has a `///` doc comment describing its purpose (`[llm.candle]` TOML section) and its relationship to `CandleInlineConfig`. (#4869) diff --git a/Cargo.lock b/Cargo.lock index 7dc398e7b..a97e43a21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,58 +92,31 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af62fb84df2af0f933d8f5fd78b843fa5eb0ec5a48fa1b528c41951d0bbe36c" -dependencies = [ - "agent-client-protocol-derive", - "agent-client-protocol-schema 0.12.0", - "anyhow", - "futures", - "futures-concurrency", - "jsonrpcmsg", - "rmcp", - "rustc-hash 2.1.2", - "schemars 1.2.1", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "tokio-util", - "tracing", - "uuid", -] - -[[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 0.13.2", + "agent-client-protocol-schema", "async-process", "blocking", "futures", "futures-concurrency", "jsonrpcmsg", - "rmcp", "rustc-hash 2.1.2", "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", @@ -151,9 +124,9 @@ dependencies = [ [[package]] name = "agent-client-protocol-schema" -version = "0.12.0" +version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49bae57dad1c28a362fbdcf7bab0583316a02b45a70792109fced55780a3b63c" +checksum = "c290bfa00c6b52339db66f8e9cf711d5f08530800529f7d619ff24d6cba253d0" dependencies = [ "anyhow", "derive_more 2.1.1", @@ -165,37 +138,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "agent-client-protocol-schema" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b957d8391ac3933e2a940446171c508d2b8ffc386d8fa7d0b9c936a2575b463e" -dependencies = [ - "anyhow", - "derive_more 2.1.1", - "schemars 1.2.1", - "serde", - "serde_json", - "serde_with", - "strum 0.28.0", - "tracing", -] - -[[package]] -name = "agent-client-protocol-tokio" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1572b219f22c4b3be0f20f934c8b6f1d1457126ce72923c4f6608f96153b65" -dependencies = [ - "agent-client-protocol 0.11.1", - "futures", - "serde", - "serde_json", - "shell-words", - "tokio", - "tokio-util", -] - [[package]] name = "ahash" version = "0.8.12" @@ -4872,7 +4814,7 @@ version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" dependencies = [ - "base64 0.22.1", + "base64 0.21.7", "chrono", "getrandom 0.2.17", "http", @@ -10582,9 +10524,8 @@ dependencies = [ name = "zeph-acp" version = "0.21.4" dependencies = [ - "agent-client-protocol 0.12.1", - "agent-client-protocol-schema 0.13.2", - "agent-client-protocol-tokio", + "agent-client-protocol", + "agent-client-protocol-schema", "async-stream", "axum 0.8.9", "base64 0.22.1", diff --git a/Cargo.toml b/Cargo.toml index e06d03a35..7b2455a70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,9 +17,8 @@ publish = true [workspace.dependencies] age = { version = "0.11.3", default-features = false } -agent-client-protocol = "0.12.1" -agent-client-protocol-schema = "0.13.2" -agent-client-protocol-tokio = "0.11.1" +agent-client-protocol = "0.14.0" +agent-client-protocol-schema = "=0.13.6" anyhow = "1.0.102" arboard = "3.6.1" arc-swap = "1.9.1" diff --git a/crates/zeph-acp/Cargo.toml b/crates/zeph-acp/Cargo.toml index fc44c5a06..6e7090080 100644 --- a/crates/zeph-acp/Cargo.toml +++ b/crates/zeph-acp/Cargo.toml @@ -17,31 +17,32 @@ default = [ "unstable-session-delete", "unstable-session-fork", "unstable-session-usage", - "unstable-session-model", "unstable-logout", "unstable-elicitation", "unstable-llm-providers", ] acp-http = ["dep:axum", "dep:blake3", "dep:dashmap", "dep:async-stream", "dep:tower-http", "dep:subtle", "dep:tower"] -# session/close is now exposed as session/delete in acp 0.12.1 -unstable-session-delete = ["agent-client-protocol/unstable_session_delete"] +# session/close and session/delete stabilized in acp 0.14.0; no upstream feature gate needed +unstable-session-delete = [] unstable-session-fork = ["agent-client-protocol/unstable_session_fork"] # session/resume is stable in acp 0.12.1; no feature gate needed unstable-session-resume = [] -unstable-session-usage = ["agent-client-protocol/unstable_session_usage"] -unstable-session-model = ["agent-client-protocol/unstable_session_model"] -unstable-elicitation = ["dep:agent-client-protocol-schema", "agent-client-protocol-schema/unstable_elicitation"] +# renamed from unstable_session_usage in acp 0.14.0; Usage struct + PromptResponse.usage remain gated +unstable-session-usage = ["agent-client-protocol/unstable_end_turn_token_usage"] +unstable-elicitation = ["dep:agent-client-protocol-schema", "agent-client-protocol-schema/unstable_elicitation", "agent-client-protocol/unstable_elicitation"] unstable-llm-providers = ["dep:agent-client-protocol-schema", "agent-client-protocol-schema/unstable_llm_providers"] -unstable-logout = ["agent-client-protocol/unstable_logout"] +# logout stabilized in acp 0.13.0; no upstream feature gate needed +unstable-logout = [] unstable-auth-methods = ["agent-client-protocol/unstable_auth_methods"] unstable-boolean-config = ["agent-client-protocol/unstable_boolean_config"] -unstable-message-id = ["agent-client-protocol/unstable_message_id"] -unstable-session-add-dirs = ["agent-client-protocol/unstable_session_additional_directories"] +# message-id stabilized in acp 0.14.0; no upstream feature gate needed +unstable-message-id = [] +# session-add-dirs stabilized in acp 0.14.0; no upstream feature gate needed +unstable-session-add-dirs = [] [dependencies] agent-client-protocol = { workspace = true } agent-client-protocol-schema = { workspace = true, optional = true } -agent-client-protocol-tokio = { workspace = true } async-stream = { workspace = true, optional = true } axum = { workspace = true, features = ["ws"], optional = true } base64.workspace = true diff --git a/crates/zeph-acp/README.md b/crates/zeph-acp/README.md index 8c77a1274..7032cbe31 100644 --- a/crates/zeph-acp/README.md +++ b/crates/zeph-acp/README.md @@ -363,7 +363,6 @@ The `initialize` response includes an `auth_hint` key in its metadata map. For s | `unstable-session-fork` | unstable | Enables the `fork_session` ACP method. See below. | | `unstable-session-resume` | unstable | Enables the `resume_session` ACP method. See below. | | `unstable-session-usage` | unstable | Enables `UsageUpdate` events — token counts (input, output, cache) sent to the IDE after each turn. See below. | -| `unstable-session-model` | unstable | Enables `SetSessionModel` — IDE-driven model switching via a native picker without `session/configure`. See below. | | `unstable-session-info-update` | unstable | Enables `SessionInfoUpdate` — agent-generated session title emitted to the IDE after the first turn. See below. | | `unstable-elicitation` | unstable | Exposes elicitation schema types (`ElicitationRequest`, etc.) for future agent-loop integration. SDK methods not yet available in 0.10.3. | | `unstable-logout` | unstable | Enables the `logout` ACP method and advertises `auth.logout` capability. Zeph logout is a no-op (vault-based auth). | @@ -379,7 +378,6 @@ zeph-acp = { version = "*", features = [ "unstable-session-fork", "unstable-session-resume", "unstable-session-usage", - "unstable-session-model", "unstable-session-info-update", "unstable-elicitation", "unstable-logout", @@ -423,16 +421,6 @@ Enables `UsageUpdate` session events. After each agent turn `ZephAcpAgent` emits The IDE can use this data to display running cost estimates or token budgets without polling a separate endpoint. -### `unstable-session-model` - -Enables `SetSessionModel` handling. When the IDE sends a `set_session_model` request (e.g., from a native model-picker dropdown), `ZephAcpAgent`: - -1. Resolves the requested `"provider:model"` key via `ProviderFactory`. -2. Stores the resolved provider in the session-scoped `provider_override`. -3. Returns a confirmation to the IDE so the picker reflects the active selection. - -This avoids the need to wrap model selection in a `session/configure` call and maps directly to the Zed AI model picker interaction. - ### `unstable-session-info-update` Enables `SessionInfoUpdate` events. After the first completed turn in a new session, `ZephAcpAgent` emits a `SessionUpdate::SessionInfoUpdate` containing a short, LLM-generated title derived from the opening message. The IDE can use this title to label the session in its sidebar or tab bar. diff --git a/crates/zeph-acp/src/agent/handlers/mod.rs b/crates/zeph-acp/src/agent/handlers/mod.rs index ab5fcedf8..4ca884154 100644 --- a/crates/zeph-acp/src/agent/handlers/mod.rs +++ b/crates/zeph-acp/src/agent/handlers/mod.rs @@ -1,14 +1,9 @@ // SPDX-FileCopyrightText: 2026 Andrei G // SPDX-License-Identifier: MIT OR Apache-2.0 -// Handler stubs for ACP 0.11 migration. Implementations land in PR 2 (#3267). -// See .local/plan/acp-migration-plan.md §4 Steps 3–4 for the full contract. - pub(crate) mod authenticate; pub(crate) mod cancel; -#[cfg(feature = "unstable-session-delete")] pub(crate) mod close_session; -#[cfg(feature = "unstable-session-delete")] pub(crate) mod delete_session; pub(crate) mod dispatch; #[cfg(feature = "unstable-session-fork")] @@ -16,13 +11,9 @@ pub(crate) mod fork_session; pub(crate) mod initialize; pub(crate) mod list_sessions; pub(crate) mod load_session; -#[cfg(feature = "unstable-logout")] pub(crate) mod logout; pub(crate) mod new_session; pub(crate) mod prompt; -#[cfg(feature = "unstable-session-resume")] pub(crate) mod resume_session; pub(crate) mod set_session_config_option; pub(crate) mod set_session_mode; -#[cfg(feature = "unstable-session-model")] -pub(crate) mod set_session_model; diff --git a/crates/zeph-acp/src/agent/handlers/set_session_model.rs b/crates/zeph-acp/src/agent/handlers/set_session_model.rs deleted file mode 100644 index c7a37cbe9..000000000 --- a/crates/zeph-acp/src/agent/handlers/set_session_model.rs +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Andrei G -// SPDX-License-Identifier: MIT OR Apache-2.0 - -//! Handler for `session/set_model` (feature `unstable-session-model`). - -use std::sync::Arc; - -use agent_client_protocol as acp; - -use crate::agent::ZephAcpAgentState; - -/// Handle an ACP `session/set_model` request. -pub(crate) async fn handle_set_session_model( - req: acp::schema::SetSessionModelRequest, - responder: acp::Responder, - _cx: acp::ConnectionTo, - state: Arc, -) -> acp::Result<()> { - let resp = state.do_set_session_model(req).await?; - responder.respond(resp) -} diff --git a/crates/zeph-acp/src/agent/mod.rs b/crates/zeph-acp/src/agent/mod.rs index ed6a202b0..08be9de7c 100644 --- a/crates/zeph-acp/src/agent/mod.rs +++ b/crates/zeph-acp/src/agent/mod.rs @@ -11,6 +11,7 @@ //! IDE capabilities (filesystem, terminal, LSP) are detected during `initialize()` and //! surfaced to the agent loop through [`AcpContext`]. +#[cfg(feature = "unstable-llm-providers")] use std::collections::{HashMap, HashSet}; use std::path::{Component, PathBuf}; use std::pin::Pin; @@ -22,6 +23,7 @@ use parking_lot::{Mutex, RwLock}; use agent_client_protocol as acp; use futures::StreamExt as _; use tokio::sync::{mpsc, oneshot}; +#[cfg(feature = "unstable-elicitation")] use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; use zeph_core::channel::{ChannelMessage, LoopbackChannel, LoopbackHandle}; @@ -471,13 +473,6 @@ pub(crate) struct SessionEntry { /// after `tool_call_update` notifications are sent (ACP requires the terminal to /// remain alive until after the notification that embeds it). pub(crate) shell_executor: Option, - /// Message-id captured at the start of a `do_prompt` turn. - /// - /// The existing one-in-flight-prompt invariant (enforced by `output_rx.lock().take()` at - /// line ~1142) guarantees at most one concurrent writer, so a plain `Mutex>` - /// is sufficient without `parking_lot`. - #[cfg(feature = "unstable-message-id")] - pub(crate) current_message_id: std::sync::Mutex>, /// Join handle for the elicitation bridge task spawned in `do_new_session`. /// /// Aborted on session close / reap for clean shutdown. `None` when the IDE @@ -566,8 +561,6 @@ pub struct ZephAcpAgentState { additional_directories_allow: Vec, /// Auth methods to advertise in the `initialize` response. MVP: always `[Agent]`. auth_methods_config: Vec, - /// When `true`, echo `PromptRequest.message_id` through responses and chunks. - message_ids_enabled: bool, /// Timeout configuration for ACP operations (terminal, elicitation, MCP bridge). pub(crate) timeouts: zeph_config::AcpTimeoutsConfig, /// Whether the IDE advertised elicitation capability during `initialize()`. @@ -620,7 +613,6 @@ impl ZephAcpAgentState { reaper_cancel: CancellationToken::new(), additional_directories_allow: Vec::new(), auth_methods_config: vec![zeph_core::config::AcpAuthMethod::Agent], - message_ids_enabled: true, timeouts: zeph_config::AcpTimeoutsConfig::default(), #[cfg(feature = "unstable-elicitation")] elicitation_supported: std::sync::atomic::AtomicBool::new(false), @@ -653,13 +645,6 @@ impl ZephAcpAgentState { self } - /// Configure message-id echo behaviour. - #[must_use] - pub fn with_message_ids_enabled(mut self, enabled: bool) -> Self { - self.message_ids_enabled = enabled; - self - } - /// Configure ACP operation timeouts. #[must_use] pub fn with_timeouts(mut self, timeouts: zeph_config::AcpTimeoutsConfig) -> Self { @@ -1141,7 +1126,6 @@ impl ZephAcpAgentState { let caps = { let mut session_caps = acp::schema::SessionCapabilities::new(); session_caps = session_caps.list(acp::schema::SessionListCapabilities::default()); - #[cfg(feature = "unstable-session-delete")] { session_caps = session_caps.close(acp::schema::SessionCloseCapabilities::default()); } @@ -1149,7 +1133,6 @@ impl ZephAcpAgentState { { session_caps = session_caps.fork(acp::schema::SessionForkCapabilities::default()); } - #[cfg(feature = "unstable-session-resume")] { session_caps = session_caps.resume(acp::schema::SessionResumeCapabilities::default()); @@ -1157,7 +1140,6 @@ impl ZephAcpAgentState { caps.session_capabilities(session_caps) }; - #[cfg(feature = "unstable-logout")] let caps = caps.auth( acp::schema::AgentAuthCapabilities::default() .logout(acp::schema::LogoutCapabilities::default()), @@ -1227,8 +1209,7 @@ impl ZephAcpAgentState { Ok(acp::schema::AuthenticateResponse::default()) } - #[cfg(feature = "unstable-logout")] - #[allow(clippy::unused_async, dead_code)] + #[allow(clippy::unused_async)] #[tracing::instrument(skip_all, name = "acp.handler.logout")] pub(crate) async fn do_logout( &self, @@ -1333,7 +1314,6 @@ impl ZephAcpAgentState { args: acp::schema::NewSessionRequest, cx: &acp::ConnectionTo, ) -> acp::Result { - #[cfg(feature = "unstable-session-add-dirs")] self.validate_additional_directories(&args.additional_directories)?; self.evict_oldest_idle_session_if_full()?; @@ -1375,6 +1355,7 @@ impl ZephAcpAgentState { ); let shell_executor = acp_ctx.shell_executor.clone(); let initial_model = self.initial_model(); + #[cfg_attr(not(feature = "unstable-elicitation"), allow(unused_mut))] let mut entry = Self::make_session_entry( handle, initial_model.clone(), @@ -1425,11 +1406,9 @@ impl ZephAcpAgentState { /// Take the `input_tx` / `output_rx` pair for a session and mark it as active. /// /// Returns an error when the session does not exist or a prompt is already in flight. - /// Also writes `turn_message_id` into the per-session slot when the feature is enabled. fn acquire_prompt_channels( &self, session_id: &acp::schema::SessionId, - #[cfg(feature = "unstable-message-id")] turn_message_id: Option<&str>, ) -> acp::Result<(mpsc::Sender, mpsc::Receiver)> { let sessions = self.sessions.lock(); let entry = sessions @@ -1441,14 +1420,6 @@ impl ZephAcpAgentState { .take() .ok_or_else(|| acp::Error::internal_error().data("prompt already in progress"))?; entry.touch(); - // Write message_id here — output_rx take succeeded, prompt will proceed. - #[cfg(feature = "unstable-message-id")] - if let Some(mid) = turn_message_id { - *entry - .current_message_id - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(mid.to_owned()); - } Ok((entry.input_tx.clone(), rx)) } @@ -1472,15 +1443,6 @@ impl ZephAcpAgentState { ) -> acp::Result { tracing::debug!(session_id = %args.session_id, "ACP prompt"); - // Capture message_id; written to per-session slot only AFTER output_rx take succeeds - // to prevent stale id from leaking when a prompt is rejected as "already in progress". - #[cfg(feature = "unstable-message-id")] - let turn_message_id: Option = if self.message_ids_enabled { - args.message_id.clone() - } else { - None - }; - // Capture session cwd for file:// boundary enforcement. let session_cwd = self .sessions @@ -1500,11 +1462,7 @@ impl ZephAcpAgentState { .await; } - let (input_tx, output_rx) = self.acquire_prompt_channels( - &args.session_id, - #[cfg(feature = "unstable-message-id")] - turn_message_id.as_deref(), - )?; + let (input_tx, output_rx) = self.acquire_prompt_channels(&args.session_id)?; self.persist_user_message_async(&args.session_id, text.clone()); @@ -1542,18 +1500,7 @@ impl ZephAcpAgentState { self.maybe_generate_session_title(&args.session_id, &text); } - // Clear per-turn message-id slot now that the turn is complete. - #[cfg(feature = "unstable-message-id")] - if let Some(entry) = self.sessions.lock().get(&args.session_id) { - *entry - .current_message_id - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) = None; - } - Ok(build_prompt_response( - #[cfg(feature = "unstable-message-id")] - turn_message_id.as_deref(), stop_reason, #[cfg(feature = "unstable-session-usage")] drain.turn_usage, @@ -1570,8 +1517,6 @@ impl ZephAcpAgentState { Ok(()) } - #[cfg(feature = "unstable-session-delete")] - #[allow(dead_code)] #[tracing::instrument(skip_all, name = "acp.handler.close_session", fields(session_id = %args.session_id))] pub(crate) async fn do_close_session( &self, @@ -1610,8 +1555,7 @@ impl ZephAcpAgentState { Ok(acp::schema::CloseSessionResponse::default()) } - #[cfg(feature = "unstable-session-delete")] - #[allow(clippy::unused_async, dead_code)] + #[allow(clippy::unused_async)] #[tracing::instrument(skip_all, name = "acp.handler.delete_session", fields(session_id = %args.session_id))] pub(crate) async fn do_delete_session( &self, @@ -1632,7 +1576,6 @@ impl ZephAcpAgentState { args: acp::schema::LoadSessionRequest, cx: &acp::ConnectionTo, ) -> acp::Result { - #[cfg(feature = "unstable-session-add-dirs")] self.validate_additional_directories(&args.additional_directories)?; if self.sessions.lock().contains_key(&args.session_id) { return Ok(acp::schema::LoadSessionResponse::new()); @@ -1780,7 +1723,6 @@ impl ZephAcpAgentState { args: acp::schema::ForkSessionRequest, cx: &acp::ConnectionTo, ) -> acp::Result { - #[cfg(feature = "unstable-session-add-dirs")] self.validate_additional_directories(&args.additional_directories)?; let in_memory = self.sessions.lock().contains_key(&args.session_id); @@ -1883,15 +1825,12 @@ impl ZephAcpAgentState { Ok(resp) } - #[cfg(feature = "unstable-session-resume")] - #[allow(dead_code)] #[tracing::instrument(skip_all, name = "acp.handler.resume_session")] pub(crate) async fn do_resume_session( &self, args: acp::schema::ResumeSessionRequest, cx: &acp::ConnectionTo, ) -> acp::Result { - #[cfg(feature = "unstable-session-add-dirs")] self.validate_additional_directories(&args.additional_directories)?; if self.sessions.lock().contains_key(&args.session_id) { return Ok(acp::schema::ResumeSessionResponse::new()); @@ -2087,7 +2026,6 @@ impl ZephAcpAgentState { /// Each requested path is canonicalized and checked with `Path::starts_with` (component-aware) /// against every entry in `self.additional_directories_allow`. Returns an `invalid_params` /// error if any path is not covered by the allowlist. - #[cfg(feature = "unstable-session-add-dirs")] fn validate_additional_directories( &self, requested: &[std::path::PathBuf], @@ -2119,53 +2057,6 @@ impl ZephAcpAgentState { } Ok(out) } - - #[cfg(feature = "unstable-session-model")] - #[allow(clippy::unused_async, dead_code)] - #[tracing::instrument(skip_all, name = "acp.handler.set_session_model")] - pub(crate) async fn do_set_session_model( - &self, - args: acp::schema::SetSessionModelRequest, - ) -> acp::Result { - let model_id: &str = &args.model_id.0; - - let Some(ref factory) = self.provider_factory else { - return Err(acp::Error::internal_error().data("model switching not configured")); - }; - - if !self - .available_models_snapshot() - .iter() - .any(|m| m == model_id) - { - return Err(acp::Error::invalid_request().data("model not in allowed list")); - } - - let Some(new_provider) = factory(model_id) else { - return Err(acp::Error::invalid_request().data("unknown model")); - }; - - { - let sessions = self.sessions.lock(); - let entry = sessions - .get(&args.session_id) - .ok_or_else(|| acp::Error::internal_error().data("session not found"))?; - *entry.provider_override.write() = Some(new_provider); - model_id.clone_into(&mut *entry.current_model.lock()); - } - - tracing::debug!(session_id = %args.session_id, model = %model_id, "ACP session model switched via set_session_model"); - - let info_update = acp::schema::SessionUpdate::SessionInfoUpdate( - acp::schema::SessionInfoUpdate::new().meta(model_meta(model_id)), - ); - self.send_notification_nowait( - &args.session_id, - acp::schema::SessionNotification::new(args.session_id.clone(), info_update), - ); - - Ok(acp::schema::SetSessionModelResponse::new()) - } } impl ZephAcpAgentState { @@ -2545,13 +2436,6 @@ impl ZephAcpAgentState { // Per-turn token totals for PromptResponse.usage (separate from session accumulator). #[cfg(feature = "unstable-session-usage")] let mut turn_usage = TurnUsage::default(); - // Capture turn message_id once per drain to avoid re-locking sessions per event. - #[cfg(feature = "unstable-message-id")] - let turn_mid: Option = self - .sessions - .lock() - .get(session_id) - .and_then(|e| e.current_message_id.lock().ok().and_then(|g| g.clone())); loop { let event = if let Some(ref signal) = cancel_signal { tokio::select! { @@ -2608,10 +2492,6 @@ impl ZephAcpAgentState { cost_cents, }; for update in loopback_event_to_updates(event) { - #[cfg(feature = "unstable-message-id")] - let update = apply_message_id_to_chunk(update, turn_mid.as_deref()); - #[cfg(not(feature = "unstable-message-id"))] - let update = update; let notification = acp::schema::SessionNotification::new(session_id.clone(), update); if let Err(e) = self.send_notification(session_id, notification).await { @@ -2638,10 +2518,6 @@ impl ZephAcpAgentState { } }); } - #[cfg(feature = "unstable-message-id")] - let update = apply_message_id_to_chunk(update, turn_mid.as_deref()); - #[cfg(not(feature = "unstable-message-id"))] - let update = update; let notification = acp::schema::SessionNotification::new(session_id.clone(), update); if let Err(e) = self.send_notification(session_id, notification).await { @@ -2845,8 +2721,6 @@ impl ZephAcpAgentState { thinking_enabled: AtomicBool::new(false), auto_approve_level: Mutex::new("suggest".to_owned()), shell_executor, - #[cfg(feature = "unstable-message-id")] - current_message_id: std::sync::Mutex::new(None), #[cfg(feature = "unstable-elicitation")] elicitation_bridge_handle: None, #[cfg(feature = "unstable-session-usage")] @@ -3017,7 +2891,7 @@ impl ZephAcpAgentState { Ok(Some(acp::schema::ExtResponse::new(raw.into()))) } "providers/set" => { - let req: schema::SetProvidersRequest = serde_json::from_str(args.params.get()) + let req: schema::SetProviderRequest = serde_json::from_str(args.params.get()) .map_err(|e| acp::Error::invalid_request().data(e.to_string()))?; let resp = self.do_set_providers(req)?; let json = serde_json::to_string(&resp) @@ -3027,8 +2901,9 @@ impl ZephAcpAgentState { Ok(Some(acp::schema::ExtResponse::new(raw.into()))) } "providers/disable" => { - let req: schema::DisableProvidersRequest = serde_json::from_str(args.params.get()) - .map_err(|e| acp::Error::invalid_request().data(e.to_string()))?; + let req: schema::DisableProviderRequest = + serde_json::from_str(args.params.get()) + .map_err(|e| acp::Error::invalid_request().data(e.to_string()))?; let resp = self.do_disable_providers(req)?; let json = serde_json::to_string(&resp) .map_err(|e| acp::Error::internal_error().data(e.to_string()))?; @@ -3097,8 +2972,8 @@ impl ZephAcpAgentState { #[tracing::instrument(skip_all, name = "acp.handler.set_providers")] pub(crate) fn do_set_providers( &self, - req: agent_client_protocol_schema::SetProvidersRequest, - ) -> acp::Result { + req: agent_client_protocol_schema::SetProviderRequest, + ) -> acp::Result { if !self.provider_names.iter().any(|(name, _)| name == &req.id) { return Err( acp::Error::invalid_params().data(format!("unknown provider id: {}", req.id)) @@ -3113,7 +2988,7 @@ impl ZephAcpAgentState { }, ); tracing::debug!(provider_id = %req.id, "provider override set"); - Ok(agent_client_protocol_schema::SetProvidersResponse::new()) + Ok(agent_client_protocol_schema::SetProviderResponse::new()) } /// Handle `providers/disable` — mark a provider as disabled for this connection. @@ -3122,17 +2997,17 @@ impl ZephAcpAgentState { /// /// # Errors /// - /// Always succeeds; returns `DisableProvidersResponse`. + /// Always succeeds; returns `DisableProviderResponse`. #[cfg(feature = "unstable-llm-providers")] #[tracing::instrument(skip_all, name = "acp.handler.disable_providers")] pub(crate) fn do_disable_providers( &self, - req: agent_client_protocol_schema::DisableProvidersRequest, - ) -> acp::Result { + req: agent_client_protocol_schema::DisableProviderRequest, + ) -> acp::Result { let id = req.id; tracing::debug!(provider_id = %id, "provider disabled"); self.global_disabled_providers.lock().insert(id); - Ok(agent_client_protocol_schema::DisableProvidersResponse::new()) + Ok(agent_client_protocol_schema::DisableProviderResponse::new()) } } @@ -3163,20 +3038,13 @@ fn compute_stop_reason(cancelled: bool, stop_hint: Option) -> acp::sch } } -/// Construct the `PromptResponse`, optionally echoing `turn_message_id` and attaching -/// per-turn token usage when the `unstable-session-usage` feature is enabled. +/// Construct the `PromptResponse`, attaching per-turn token usage when the +/// `unstable-session-usage` feature is enabled. fn build_prompt_response( - #[cfg(feature = "unstable-message-id")] turn_message_id: Option<&str>, stop_reason: acp::schema::StopReason, #[cfg(feature = "unstable-session-usage")] turn_usage: TurnUsage, ) -> acp::schema::PromptResponse { let r = acp::schema::PromptResponse::new(stop_reason); - #[cfg(feature = "unstable-message-id")] - let r = if let Some(mid) = turn_message_id { - r.user_message_id(mid.to_owned()) - } else { - r - }; #[cfg(feature = "unstable-session-usage")] let r = { let total = turn_usage @@ -3269,21 +3137,12 @@ pub async fn run_agent( state: Arc, transport: impl acp::ConnectTo, ) -> acp::Result<()> { - #[cfg(feature = "unstable-session-delete")] - use handlers::close_session; - #[cfg(feature = "unstable-session-delete")] - use handlers::delete_session; #[cfg(feature = "unstable-session-fork")] use handlers::fork_session; - #[cfg(feature = "unstable-logout")] - use handlers::logout; - #[cfg(feature = "unstable-session-resume")] - use handlers::resume_session; - #[cfg(feature = "unstable-session-model")] - use handlers::set_session_model; use handlers::{ - authenticate, cancel, dispatch, initialize, list_sessions, load_session, new_session, - prompt, set_session_config_option, set_session_mode, + authenticate, cancel, close_session, delete_session, dispatch, initialize, list_sessions, + load_session, logout, new_session, prompt, resume_session, set_session_config_option, + set_session_mode, }; let builder = acp::Agent @@ -3328,12 +3187,10 @@ pub async fn run_agent( acp::on_receive_notification!(), ); - #[cfg(feature = "unstable-session-delete")] let builder = builder.on_receive_request( req_handler!(state, close_session::handle_close_session), acp::on_receive_request!(), ); - #[cfg(feature = "unstable-session-delete")] let builder = builder.on_receive_request( req_handler!(state, delete_session::handle_delete_session), acp::on_receive_request!(), @@ -3343,17 +3200,10 @@ pub async fn run_agent( req_handler!(state, fork_session::handle_fork_session), acp::on_receive_request!(), ); - #[cfg(feature = "unstable-session-resume")] let builder = builder.on_receive_request( req_handler!(state, resume_session::handle_resume_session), acp::on_receive_request!(), ); - #[cfg(feature = "unstable-session-model")] - let builder = builder.on_receive_request( - req_handler!(state, set_session_model::handle_set_session_model), - acp::on_receive_request!(), - ); - #[cfg(feature = "unstable-logout")] let builder = builder.on_receive_request( req_handler!(state, logout::handle_logout), acp::on_receive_request!(), @@ -3374,30 +3224,6 @@ pub async fn run_agent( .await } -/// Attach `message_id` to `AgentMessageChunk`, `UserMessageChunk`, and `AgentThoughtChunk` -/// updates when a message id is present for this turn. -#[cfg(feature = "unstable-message-id")] -fn apply_message_id_to_chunk( - update: acp::schema::SessionUpdate, - message_id: Option<&str>, -) -> acp::schema::SessionUpdate { - let Some(mid) = message_id else { - return update; - }; - match update { - acp::schema::SessionUpdate::AgentMessageChunk(chunk) => { - acp::schema::SessionUpdate::AgentMessageChunk(chunk.message_id(mid.to_owned())) - } - acp::schema::SessionUpdate::UserMessageChunk(chunk) => { - acp::schema::SessionUpdate::UserMessageChunk(chunk.message_id(mid.to_owned())) - } - acp::schema::SessionUpdate::AgentThoughtChunk(chunk) => { - acp::schema::SessionUpdate::AgentThoughtChunk(chunk.message_id(mid.to_owned())) - } - other => other, - } -} - /// Compile-time assertions that ACP state and executors are `Send + Sync`. const _: () = { #[allow(clippy::used_underscore_items)] @@ -3481,7 +3307,7 @@ mod providers_tests { fn disable_provider_hides_current_config_in_list() { let state = make_state(); state - .do_disable_providers(schema::DisableProvidersRequest::new("openai")) + .do_disable_providers(schema::DisableProviderRequest::new("openai")) .unwrap(); let resp = state .do_list_providers(schema::ListProvidersRequest::new()) @@ -3502,7 +3328,7 @@ mod providers_tests { fn disable_unknown_provider_succeeds() { let state = make_state(); state - .do_disable_providers(schema::DisableProvidersRequest::new("nonexistent")) + .do_disable_providers(schema::DisableProviderRequest::new("nonexistent")) .unwrap(); } @@ -3511,7 +3337,7 @@ mod providers_tests { let state = make_state(); let err = state .do_set_providers( - schema::SetProvidersRequest::new( + schema::SetProviderRequest::new( "unknown_provider", schema::LlmProtocol::OpenAi, "https://evil.example.com", @@ -3531,7 +3357,7 @@ mod providers_tests { let state = make_state(); state .do_set_providers( - schema::SetProvidersRequest::new( + schema::SetProviderRequest::new( "openai", schema::LlmProtocol::OpenAi, "https://custom.example.com", @@ -3552,7 +3378,7 @@ mod providers_tests { let state = make_state(); state .do_set_providers( - schema::SetProvidersRequest::new( + schema::SetProviderRequest::new( "openai", schema::LlmProtocol::OpenAi, "https://custom.example.com", @@ -3561,7 +3387,7 @@ mod providers_tests { ) .unwrap(); state - .do_disable_providers(schema::DisableProvidersRequest::new("openai")) + .do_disable_providers(schema::DisableProviderRequest::new("openai")) .unwrap(); let resp = state .do_list_providers(schema::ListProvidersRequest::new()) @@ -3574,56 +3400,6 @@ mod providers_tests { } } -#[cfg(all(test, feature = "unstable-message-id"))] -mod message_id_tests { - use super::*; - - fn agent_chunk(text: &str) -> acp::schema::SessionUpdate { - acp::schema::SessionUpdate::AgentMessageChunk(acp::schema::ContentChunk::new( - text.to_owned().into(), - )) - } - - fn user_chunk(text: &str) -> acp::schema::SessionUpdate { - acp::schema::SessionUpdate::UserMessageChunk(acp::schema::ContentChunk::new( - text.to_owned().into(), - )) - } - - #[test] - fn apply_sets_message_id_on_agent_chunk() { - let update = agent_chunk("hello"); - let result = apply_message_id_to_chunk(update, Some("msg-001")); - if let acp::schema::SessionUpdate::AgentMessageChunk(chunk) = result { - assert_eq!(chunk.message_id, Some("msg-001".to_owned())); - } else { - panic!("expected AgentMessageChunk"); - } - } - - #[test] - fn apply_sets_message_id_on_user_chunk() { - let update = user_chunk("hi"); - let result = apply_message_id_to_chunk(update, Some("msg-002")); - if let acp::schema::SessionUpdate::UserMessageChunk(chunk) = result { - assert_eq!(chunk.message_id, Some("msg-002".to_owned())); - } else { - panic!("expected UserMessageChunk"); - } - } - - #[test] - fn apply_none_message_id_is_noop() { - let update = agent_chunk("hello"); - let result = apply_message_id_to_chunk(update, None); - if let acp::schema::SessionUpdate::AgentMessageChunk(chunk) = result { - assert_eq!(chunk.message_id, None); - } else { - panic!("expected AgentMessageChunk"); - } - } -} - #[cfg(all(test, feature = "unstable-session-usage"))] mod usage_tests { use super::*; @@ -3657,12 +3433,7 @@ mod usage_tests { cache_read_tokens: 10, cache_write_tokens: 0, }; - let resp = build_prompt_response( - #[cfg(feature = "unstable-message-id")] - None, - acp::schema::StopReason::EndTurn, - turn_usage, - ); + let resp = build_prompt_response(acp::schema::StopReason::EndTurn, turn_usage); let u = resp.usage.expect("usage should be set"); assert_eq!(u.total_tokens, 150); assert_eq!(u.input_tokens, 100); @@ -3678,12 +3449,7 @@ mod usage_tests { #[test] fn build_prompt_response_zero_usage_still_attaches() { let turn_usage = TurnUsage::default(); - let resp = build_prompt_response( - #[cfg(feature = "unstable-message-id")] - None, - acp::schema::StopReason::EndTurn, - turn_usage, - ); + let resp = build_prompt_response(acp::schema::StopReason::EndTurn, turn_usage); let u = resp .usage .expect("usage should be set even for zero tokens"); diff --git a/crates/zeph-acp/src/agent/tests.rs b/crates/zeph-acp/src/agent/tests.rs index f6ee1be79..ecae9efd3 100644 --- a/crates/zeph-acp/src/agent/tests.rs +++ b/crates/zeph-acp/src/agent/tests.rs @@ -1468,7 +1468,6 @@ async fn initialize_advertises_session_capabilities() { session_caps.resume.is_some(), "resume capability must be advertised" ); - #[cfg(feature = "unstable-session-delete")] assert!( session_caps.close.is_some(), "close capability must be advertised" @@ -1961,7 +1960,6 @@ async fn fork_session_creates_new_session_from_existing() { .await; } -#[cfg(feature = "unstable-session-resume")] #[tokio::test] async fn resume_session_returns_ok_for_active() { let local = tokio::task::LocalSet::new(); @@ -1983,7 +1981,6 @@ async fn resume_session_returns_ok_for_active() { .await; } -#[cfg(feature = "unstable-session-resume")] #[tokio::test] async fn resume_session_errors_for_unknown() { let local = tokio::task::LocalSet::new(); @@ -2334,62 +2331,6 @@ fn loopback_tool_output_multiline_preserves_newlines_in_terminal_data() { } } -// --- #958 SetSessionModel --- - -#[cfg(feature = "unstable-session-model")] -#[tokio::test] -async fn set_session_model_no_factory_errors() { - let local = tokio::task::LocalSet::new(); - local - .run_until(async { - let (agent, mut rx) = make_agent(); - let resp = agent - .new_session(acp::NewSessionRequest::new(std::path::PathBuf::from("."))) - .await - .unwrap(); - while let Ok((_, ack)) = rx.try_recv() { - let _ = ack.send(()); - } - let result = agent - .set_session_model(acp::SetSessionModelRequest::new( - resp.session_id, - "some:model", - )) - .await; - assert!(result.is_err()); - }) - .await; -} - -#[cfg(feature = "unstable-session-model")] -#[tokio::test] -async fn set_session_model_rejects_unknown_model() { - let local = tokio::task::LocalSet::new(); - local - .run_until(async { - let (tx, _rx) = tokio::sync::mpsc::unbounded_channel(); - let conn_slot = std::rc::Rc::new(std::cell::RefCell::new(None)); - let factory: ProviderFactory = Arc::new(|_| None); - let agent = ZephAcpAgent::new(make_spawner(), tx, conn_slot, 4, 1800, None) - .with_provider_factory( - factory, - shared_models(vec!["claude:claude-sonnet-4-6".to_owned()]), - ); - let resp = agent - .new_session(acp::NewSessionRequest::new(std::path::PathBuf::from("."))) - .await - .unwrap(); - let result = agent - .set_session_model(acp::SetSessionModelRequest::new( - resp.session_id, - "ollama:llama3", - )) - .await; - assert!(result.is_err()); - }) - .await; -} - #[tokio::test] async fn new_session_meta_contains_project_rules() { let local = tokio::task::LocalSet::new(); @@ -3789,7 +3730,6 @@ async fn non_llm_slash_commands_all_complete_without_hanging() { .await; } -#[cfg(feature = "unstable-session-delete")] #[tokio::test] async fn close_session_removes_entry() { let local = tokio::task::LocalSet::new(); @@ -3819,7 +3759,6 @@ async fn close_session_removes_entry() { .await; } -#[cfg(feature = "unstable-session-delete")] #[tokio::test] async fn close_session_unknown_id_is_ok() { let local = tokio::task::LocalSet::new(); @@ -3948,7 +3887,6 @@ async fn set_config_option_model_emits_session_info_update() { .await; } -#[cfg(feature = "unstable-session-delete")] #[tokio::test] async fn close_session_signals_cancel() { let local = tokio::task::LocalSet::new(); diff --git a/crates/zeph-acp/src/lib.rs b/crates/zeph-acp/src/lib.rs index b4e81a602..05edb8c09 100644 --- a/crates/zeph-acp/src/lib.rs +++ b/crates/zeph-acp/src/lib.rs @@ -38,7 +38,6 @@ //! | `unstable-session-fork` | ACP session fork extension | //! | `unstable-session-resume` | ACP session resume (stable since acp 0.12.1; no SDK gate needed) | //! | `unstable-session-usage` | ACP session token-usage extension | -//! | `unstable-session-model` | ACP session model-switching extension | //! | `unstable-elicitation` | ACP elicitation schema types | //! | `unstable-logout` | ACP logout extension | //! diff --git a/crates/zeph-acp/src/transport/mod.rs b/crates/zeph-acp/src/transport/mod.rs index ee816e9f1..4188485e2 100644 --- a/crates/zeph-acp/src/transport/mod.rs +++ b/crates/zeph-acp/src/transport/mod.rs @@ -122,7 +122,7 @@ pub struct AcpServerConfig { pub additional_directories: Vec, /// Auth methods to advertise in the `initialize` response. pub auth_methods: Vec, - /// When `true`, echo `PromptRequest.message_id` through responses and chunks. + /// Retained for config compatibility. `PromptRequest.message_id` was removed in acp 0.14.0. pub message_ids_enabled: bool, /// Per-request timeout configuration for elicitation, terminal, and MCP operations. pub timeouts: zeph_config::AcpTimeoutsConfig, diff --git a/crates/zeph-acp/src/transport/stdio.rs b/crates/zeph-acp/src/transport/stdio.rs index dfac024e1..e0a420d22 100644 --- a/crates/zeph-acp/src/transport/stdio.rs +++ b/crates/zeph-acp/src/transport/stdio.rs @@ -106,7 +106,6 @@ pub(crate) async fn build_agent_state( if !server_config.auth_methods.is_empty() { agent = agent.with_auth_methods(server_config.auth_methods); } - agent = agent.with_message_ids_enabled(server_config.message_ids_enabled); agent = agent.with_timeouts(server_config.timeouts); let state = Arc::new(agent); diff --git a/specs/013-acp/spec.md b/specs/013-acp/spec.md index 662239822..819548545 100644 --- a/specs/013-acp/spec.md +++ b/specs/013-acp/spec.md @@ -8,7 +8,7 @@ tags: - protocol - acp created: 2026-04-08 -updated: 2026-05-19 +updated: 2026-06-06 status: approved related: - "[[MOC-specs]]" @@ -19,7 +19,7 @@ related: > [!info] > ACP transports, session management, permissions, fork/resume, -> capability advertisement, agent-client-protocol 0.12.1 / schema 0.13.2 compatibility. +> capability advertisement, agent-client-protocol 0.14.0 / schema =0.13.6 compatibility. ## Spec Changelog @@ -28,6 +28,7 @@ related: | 1.0 | 2026-04-08 | sdd | Initial spec (SDK 0.11.1 / schema 0.12.0) | | 1.1 | 2026-05-19 | sdd | Updated to SDK 0.12.1 / schema 0.13.2; added Providers API, Elicitation, MCP-over-ACP, Session Usage, Session Delete migration, v2 tracking, breaking changes resolution | | 1.2 | 2026-05-29 | sdd | Mark Providers API, Elicitation protocol, Session Usage, and session/delete as implemented; update SDK to 0.12.1; wire IDE-provided MCP servers into do_new_session; add blocking-await timeout note | +| 1.3 | 2026-06-06 | sdd | ACP 0.14.0 protocol bump: bumped core 0.12.1→0.14.0, schema pinned =0.13.6; removed session/set_model RPC (model switching preserved via set_config_option); removed inbound message-id echo feature; renamed provider ext-method types to singular; stabilized delete/logout/resume/add-dirs feature flags; renamed session-usage upstream gate; added elicitation core passthrough; documented MessageId newtype change | --- @@ -81,21 +82,22 @@ AcpSessionManager - Session fork: create a new session branching from an existing session at a given turn - Session resume: reconnect to an existing session by ID -### Agent Spawner Contract (0.12.1) +### Agent Spawner Contract (0.14.0) -Agent sessions use the `Agent.builder()` / `run_agent()` pattern from -`agent-client-protocol 0.11.1`, preserved in 0.12.1. Session state is `Arc`-wrapped. +Agent sessions use the `Agent.builder()` / `run_agent()` pattern. Session state is `Arc`-wrapped. Session tasks are launched via `tokio::task::spawn_local` inside a `LocalSet` — the `AgentSpawner` closure returns `Pin + 'static>>` (`!Send`). -SDK 0.12.0 removed `McpAcpTransport` and the direct `tokio` re-export. Zeph is unaffected: -`McpAcpTransport` was never used, and Zeph has its own `tokio` dependency. +SDK 0.12.0 removed `McpAcpTransport` and the direct `tokio` re-export; the dead +`agent-client-protocol-tokio` crate was also removed entirely in the 0.14.0 bump. +Zeph is unaffected: `McpAcpTransport` was never used, Zeph has its own `tokio` dependency, +and `agent-client-protocol-tokio` was removed from both workspace `Cargo.toml` and `crates/zeph-acp/Cargo.toml`. -`session/close` and `session/resume` were stabilized in SDK 0.12.0 (schema 0.12.2). -The `unstable-session-resume` and `unstable-session-close` feature flags in Zeph should be -removed after SDK upgrade to 0.12.1. +`session/close`, `session/resume`, `session/delete`, and `session/logout` are unconditional in +core 0.14.0. The corresponding `unstable-session-*` Zeph feature flags are tombstoned as +no-op `= []` (retained only so root `Cargo.toml` forwarding resolves without changes). -**Status: implemented** (SDK upgraded to 0.12.1 in commit #4464; `unstable-session-resume` and `unstable-session-close` feature flags removed) +**Status: implemented** (SDK upgraded to 0.14.0 / schema =0.13.6) ## Permission Model @@ -114,7 +116,7 @@ AcpPermissionGate (TOML-backed, SQLite-persisted) ## Protocol Messages - Rich content: images, file resources, binary data -- Model switching: client can request a specific model per session +- Model switching: client requests a specific model via `session/set_config_option` with `config_id="model"` (see Model Switching below) - Terminal forwarding: tool output streams back to IDE terminal - File tools: read/write/list within session working directory - MCP passthrough: MCP tools are forwarded to ACP client via `mcp_passthrough` capability @@ -129,9 +131,13 @@ are available in PR4+: | `enabled` | bool | `false` | Enable ACP server | | `agent_name` | String | `"zeph"` | Agent name advertised to clients | | `transport` | String | `"stdio"` | Transport: `stdio`, `http`, `ws`, `both` | -| `additional_directories` | `Vec` | `[]` | **Request-side allowlist.** Paths a client may pass in `sessionInit.additionalDirectories`. Paths not in this list are rejected at session start. This is NOT a protocol advertisement — it is a server-side gate. | +| `additional_directories` | `Vec` | `[]` | **Request-side allowlist.** Paths a client may pass in `sessionInit.additionalDirectories`. Paths not in this list are rejected at session start. This is NOT a protocol advertisement — it is a server-side gate. Field is unconditional (degated in 0.14.0 bump). | | `auth_methods` | `Vec` | `["agent"]` | Accepted authentication methods. MVP: only `"agent"` is valid. Unknown values are rejected at deserialization. | -| `message_ids_enabled` | bool | `true` | Echo client-supplied `message_id` in `PromptResponse.user_message_id` and all streamed chunks. | + +> **Changed in 0.14.0 bump**: `message_ids_enabled` is retained as a no-op field for config-schema +> compatibility (read by `acp_commands.rs`). The `PromptRequest.message_id` and +> `PromptResponse.user_message_id` protocol fields were deleted upstream in schema 0.13.6; the +> inbound message-id echo behaviour is removed. ### Key Invariants @@ -140,8 +146,6 @@ are available in PR4+: `AcpError::PermissionDenied` at session start — never silently ignored - `auth_methods` must only contain `"agent"` for MVP; unknown variants cause a hard deserialization error at startup to prevent misconfigured deployments from silently accepting unexpected auth -- When `message_ids_enabled = true`, every `PromptResponse` and every streamed chunk must carry the - originating `message_id` — partial echo (response but not chunks, or vice versa) is a bug ## Session CRUD Endpoints (#3902, #4252) @@ -183,7 +187,7 @@ error — session terminated due to an unhandled error ### session/close -**Status: stable** (stabilized in schema 0.12.2, SDK 0.12.0) +**Status: stable** (stabilized in schema 0.12.2, SDK 0.12.0; unconditional in core 0.14.0) `session/close` handler gracefully terminates an ACP session: flushes pending memory writes, cancels in-flight tool calls, persists session state to SQLite, and removes the session from @@ -200,13 +204,29 @@ string for diagnostics (e.g., `"user_initiated"`, `"timeout"`, `"error"`). ### session/resume -**Status: stable** (stabilized in schema 0.12.2, SDK 0.12.0) +**Status: stable** (stabilized in schema 0.12.2, SDK 0.12.0; unconditional in core 0.14.0) Reconnect to an existing session by ID, restoring conversation history and tool context. Previously gated behind `unstable-session-resume` feature flag in Zeph. -**Action required on SDK upgrade**: remove `unstable-session-resume` feature flag from -`crates/zeph-acp/Cargo.toml` and root `Cargo.toml`. Use the stable API directly. +The `unstable-session-resume` Zeph feature flag is now a tombstone `= []`. All `#[cfg(feature = +"unstable-session-resume")]` gates are removed; the resume handler runs unconditionally. + +### session/delete + +**Status: stable** (unconditional in core 0.14.0) + +Remove a session from the `session/list` registry. Previously gated behind `unstable-session-delete`. +The `unstable-session-delete` Zeph feature flag is now a tombstone `= []`. All cfg gates removed. + +Custom `_session/delete` extension (backward compat) is retained alongside the standard method. + +### session/logout + +**Status: stable** (unconditional in core 0.14.0) + +Previously gated behind `unstable-logout`. The `unstable-logout` Zeph feature flag is now a +tombstone `= []`. All cfg gates removed; logout handler runs unconditionally. ### Capability Negotiation @@ -219,11 +239,10 @@ ACP server advertises its capabilities in the `initialize` response and via the `GET /agent.json` returns a JSON document describing the agent's identity, declared capabilities, supported protocol version, and authentication methods. This endpoint is unauthenticated and used by IDE clients for discovery. ```json -// after SDK upgrade to 0.12.1 / schema 0.13.2 (see I1) { "name": "...", "version": "...", - "protocol": "acp/0.13.2", + "protocol": "acp/0.13.6", "capabilities": ["tools", "memory", "streaming"], "authMethods": ["bearer"] } @@ -231,8 +250,8 @@ ACP server advertises its capabilities in the `initialize` response and via the #### Protocol Version -Zeph uses `agent-client-protocol 0.12.1` / `schema 0.13.2` (upgraded in commit #4464). -The `/agent.json` `protocol` field reflects `"acp/0.13.2"` per the compiled crate version. +Zeph uses `agent-client-protocol 0.14.0` / `schema =0.13.6`. +The `/agent.json` `protocol` field reflects `"acp/0.13.6"` per the compiled schema crate version. #### Current Model in SessionInfoUpdate @@ -257,13 +276,73 @@ exposing tools over ACP. --- -## Unstable Features (feature: `acp-unstable`) +## Feature Flags + +| Flag | Status | Notes | +|------|--------|-------| +| `unstable-session-fork` | **active** | Still gated upstream (`unstable_session_fork`) | +| `unstable-session-usage` | **active** | Gate renamed upstream: now forwards `agent-client-protocol/unstable_end_turn_token_usage` (was `unstable_session_usage`). `Usage` struct + `PromptResponse.usage` field are ALL gated — not unconditional. | +| `unstable-elicitation` | **active** | Now also adds `agent-client-protocol/unstable_elicitation` passthrough so core wires `elicitation/create` | +| `unstable-llm-providers` | **active** | Still gated upstream (`unstable_llm_providers`); provider type renames apply here (see Providers API) | +| `unstable-auth-methods` | **active** | Still gated upstream (`unstable_auth_methods`) | +| `unstable-boolean-config` | **active** | Still gated upstream (`unstable_boolean_config`) | +| `unstable-session-delete` | **tombstone** `= []` | Stabilized — `session/delete` handler is unconditional in core 0.14.0. Flag retained as no-op for workspace forwarding (root `Cargo.toml` references it). | +| `unstable-session-resume` | **tombstone** `= []` | Stabilized — `session/resume` handler is unconditional in core 0.14.0. Flag retained as no-op. | +| `unstable-logout` | **tombstone** `= []` | Stabilized — logout handler is unconditional in core 0.14.0. Flag retained as no-op. | +| `unstable-session-add-dirs` | **tombstone** `= []` | Stabilized — `additional_directories` field is plain `Vec`, unconditional in schema 0.13.6. Flag retained as no-op. | +| `unstable-message-id` | **tombstone** `= []` | Removed — `PromptRequest.message_id` and `PromptResponse.user_message_id` deleted upstream. Entire inbound echo feature removed. Flag retained as no-op for workspace forwarding. | +| `unstable-session-model` | **DELETED** | Removed entirely — `session/set_model` RPC deleted upstream. Feature name removed from Cargo.toml and root `Cargo.toml`. Model switching survives via `set_config_option`. | + +> **Tombstone flags** are `= []` no-ops retained solely so root `Cargo.toml` feature forwarding +> resolves without changes. They add zero behavior. + +--- + +## Model Switching + +**Status: preserved via stable mechanism** + +The dedicated `session/set_model` RPC method was removed upstream (deleted in `agent-client-protocol` +0.14.0 / schema 0.13.6). This is NOT a capability loss. + +Model switching is FULLY preserved via two stable paths: + +1. **`session/set_config_option`** with `config_id="model"` and `value=` — the + canonical stable path. Runs identical logic to the former `session/set_model`: calls + `provider_factory(value)`, validates against `available_models_snapshot()`, updates + `provider_override`, and emits `SessionInfoUpdate` with `model_meta`. +2. **`$/model` slash command** — IDE/CLI convenience; internally dispatches to the same + `apply_session_config` path. + +`session/set_mode` (behavioral persona switch: `code`/`architect`/`ask`) is an orthogonal +concept, NOT a replacement for model switching. Mode and model are independent. + +> **NEVER** describe the removal of `session/set_model` as a capability loss. Model switching +> survives unconditionally via `session/set_config_option`. + +--- + +## Message ID Echo (REMOVED) + +**Status: removed in 0.14.0 bump** + +`PromptRequest.message_id` and `PromptResponse.user_message_id` were deleted upstream in +schema 0.13.6. The entire inbound message-id echo feature is removed from Zeph: + +- `message_ids_enabled` config field retained as no-op (config-schema compatibility) +- `current_message_id` session slot removed +- `build_prompt_response` no longer accepts or echoes a message ID +- `apply_message_id_to_chunk` removed (no live data source) +- `unstable-message-id` feature is a tombstone `= []` + +`ContentChunk.message_id` field still exists in schema 0.13.6 for potential future +agent-generated per-chunk IDs, but Zeph does not inject it (no inbound source). -- `unstable-session-list`: enumerate active sessions *(was already stable at 0.11.1)* -- `unstable-session-fork`: fork session at a point +### MessageId Type -> **Note**: `unstable-session-resume` and `unstable-session-close` are no longer unstable -> upstream (stabilized in schema 0.12.2). These flags should be removed after SDK upgrade to 0.12.1. +In schema 0.13.6, `MessageId` is a newtype: `MessageId(pub Arc)`. The chunk builder +accepts `impl IntoOption`, where `IntoOption` is implemented for +`&str` **only** (not `String`). Passing `String` will not compile — always pass `&str`. --- @@ -281,6 +360,18 @@ Schema 0.11.7 introduced a providers management API (`unstable` in SDK): | `providers/set` | Sets the active provider for the session | | `providers/disable` | Disables a provider for the session | +**Breaking change in 0.14.0 bump — type renames (singular):** + +| Old type name | New type name | +|---------------|---------------| +| `SetProvidersRequest` | `SetProviderRequest` | +| `SetProvidersResponse` | `SetProviderResponse` | +| `DisableProvidersRequest` | `DisableProviderRequest` | +| `DisableProvidersResponse` | `DisableProviderResponse` | + +All renamed types have `::new()` constructors. All four remain gated behind +`unstable_llm_providers` (Zeph flag `unstable-llm-providers` retained). + **Design note — impedance mismatch**: The Providers API is NOT a direct mapping to Zeph's `[[llm.providers]]` TOML config. Key tensions: @@ -319,11 +410,10 @@ Schema 0.11.5 introduced structured user input (elicitation) across three scopes - **Request scope** (0.11.5, PR #771): agent requests structured input during prompt processing - **Scoped by mode** (0.11.6, PR #966): elicitation behavior varies by mode -**Current Zeph state**: `unstable-elicitation = []` in `crates/zeph-acp/Cargo.toml` is a -**local empty feature flag** — it does NOT pass through to `agent-client-protocol/unstable_elicitation`. -The SDK 0.11.1 has no corresponding feature flag. This means elicitation in Zeph is not -SDK-gated; it requires a custom implementation or will need to align with SDK 0.12.x's -elicitation support. This is NOT a simple "enable a feature flag" task. +**Current Zeph state**: `unstable-elicitation` in `crates/zeph-acp/Cargo.toml` now includes +`agent-client-protocol/unstable_elicitation` passthrough (added in the 0.14.0 bump). This wires +core's `elicitation/create` request dispatch path. Zeph already implements elicitation in +`elicitation.rs`; the core passthrough ensures `elicitation/create` is registered. **Fixed**: `elicitation_timeout_secs` is now read from `_meta` in `mcp_bridge.rs` (commit #4453). `elicitation_enabled` is read from `_meta` rather than being hardcoded to `false` (commit #4441). @@ -424,9 +514,25 @@ without `_` prefix are rejected). | `McpAcpTransport` struct removed | Zeph does not use `McpAcpTransport` (grep confirmed) | **Resolved — no action** | | `McpConnectRequest.acp_url` renamed to `acp_id` | Zeph does not use `acp_url` (grep confirmed) | **Resolved — no action** | | `tokio` re-export removed from SDK | Zeph uses its own `tokio` dependency — does not import tokio types from the SDK (grep confirmed) | **Resolved — no action** | -| `session/close` and `session/resume` stabilized | Zeph uses feature flags `unstable-session-close` and `unstable-session-resume` — remove after SDK upgrade | **Pending SDK upgrade** | +| `session/close` and `session/resume` stabilized | Feature flags removed; handlers unconditional | **Resolved** | | `_` prefix required for extension methods | Zeph's custom extension is already `_session/delete` | **Resolved — compliant** | +## Breaking Changes Resolution (SDK 0.12.1 → 0.14.0) + +| Breaking Change | Impact on Zeph | Status | +|----------------|---------------|--------| +| `agent-client-protocol` bumped to `0.14.0`, schema pinned `=0.13.6` | Workspace `Cargo.toml` updated; `=` pin required for schema | **Resolved** | +| `agent-client-protocol-tokio` dead dep removed | Dep line deleted from workspace + crate `Cargo.toml` | **Resolved** | +| `session/set_model` RPC deleted upstream | Handler + file + tests deleted; model switching preserved via `session/set_config_option` (config_id="model") | **Resolved** | +| `PromptRequest.message_id` removed upstream | Entire inbound message-id echo feature removed; `unstable-message-id` tombstoned | **Resolved** | +| `PromptResponse.user_message_id` removed upstream | Removed from `build_prompt_response`; was a hard compile break | **Resolved** | +| `SetProvidersRequest/Response` → `SetProviderRequest/Response` (singular) | Renamed at all ext-method dispatch sites | **Resolved** | +| `DisableProvidersRequest/Response` → `DisableProviderRequest/Response` (singular) | Renamed at all ext-method dispatch sites | **Resolved** | +| `unstable_session_usage` gate renamed to `unstable_end_turn_token_usage` | `unstable-session-usage` feature re-pointed; `Usage` struct + `PromptResponse.usage` still gated | **Resolved** | +| `unstable_elicitation` added to core 0.14.0 | `unstable-elicitation` feature now passes through to core | **Resolved** | +| `MessageId` type changed to newtype `MessageId(pub Arc)` | `IntoOption` impl for `&str` only — no `String` | **Resolved** | +| `session/delete`, `session/resume`, `session/logout`, `additional_directories` stabilized | Feature flags tombstoned `= []`; all cfg gates removed | **Resolved** | + --- ## Implementation Gap Tracker @@ -442,10 +548,16 @@ without `_` prefix are rejected). | I7 | Session usage reporting | **Implemented** (#4522) | ✓ Done | — | | I8 | `elicitation_timeout_secs` hardcoded | **Fixed** — read from `_meta` (#4453) | ✓ Done | — | | I9 | Shell timeout hardcoded | 10+ sites in `terminal.rs` with 120s | `[acp.timeouts]` config section | P3 | -| I10 | Logout method | `handlers/logout.rs` exists | Verify against upstream Preview RFD | P3 | +| I10 | Logout method | **Stable** — degated in 0.14.0 bump | ✓ Done | — | | I11 | Agent telemetry export | Local tracing only | Follow upstream RFD (not yet in schema) | P4 | | I12 | IDE-provided MCP servers | **Implemented** — wired into `do_new_session` (#4444) | ✓ Done | — | | I13 | Blocking awaits in handlers | **Fixed** — bounded with configurable timeouts (#4538) | ✓ Done | — | +| I14 | SDK upgrade 0.12.1 → 0.14.0 | **In progress** (this PR) | ✓ Done (pending merge) | — | +| I15 | Remove `session/set_model` handler | **In progress** (this PR) | ✓ Done (pending merge) | — | +| I16 | Remove inbound message-id echo | **In progress** (this PR) | ✓ Done (pending merge) | — | +| I17 | Provider type renames (singular) | **In progress** (this PR) | ✓ Done (pending merge) | — | +| I18 | Re-point `unstable-session-usage` gate | **In progress** (this PR) | ✓ Done (pending merge) | — | +| I19 | Add elicitation core passthrough | **In progress** (this PR) | ✓ Done (pending merge) | — | --- @@ -539,33 +651,34 @@ No action needed now. Monitor upstream v2 progress at https://github.com/agentcl --- -## Addendum: Interop Protocol Gap Analysis (2026-04-17, updated 2026-05-19) +## Addendum: Interop Protocol Gap Analysis (2026-04-17, updated 2026-06-06) Cross-reference: `specs/045-interop-protocol-gaps/spec.md` ### ACP Baseline vs. arXiv:2505.02279 Survey -Zeph's ACP implementation is currently based on `agent-client-protocol = "0.11.1"` (workspace -`Cargo.toml`). Current upstream: SDK **0.12.1** / schema **0.13.2** (2026-05-17). +Zeph's ACP implementation is based on `agent-client-protocol = "0.14.0"` / schema `=0.13.6` +(workspace `Cargo.toml`, updated in this PR). The survey (arXiv:2505.02279) describes ACP's capability advertisement and re-negotiation model as a differentiating feature vs. MCP and A2A. -**Capability re-negotiation status: Unverified.** The `agent-client-protocol` 0.11 SDK -includes capability fields in the session handshake message. Dynamic re-negotiation during -an active session has not been confirmed tested in Zeph's `AcpSessionManager`. +**Capability re-negotiation status: Unverified.** Dynamic re-negotiation during an active +session has not been confirmed tested in Zeph's `AcpSessionManager`. This does not block any current feature. It is tracked as a P3 follow-up in `specs/045-interop-protocol-gaps/spec.md` under "P3 Follow-up: ACP capability re-negotiation integration test". -### Version Upgrade Note - -To upgrade `agent-client-protocol` from 0.11.1 to 0.12.1: -1. Review breaking changes in the SDK changelog (summarized in Breaking Changes Resolution table above). -2. Update `Cargo.toml` workspace dependency: `agent-client-protocol = "0.12.1"`, `agent-client-protocol-tokio = "0.12.1"`. -3. Remove `unstable-session-resume` and `unstable-session-close` feature flags from `crates/zeph-acp/Cargo.toml` and root `Cargo.toml`. -4. Run `cargo nextest run --workspace --features full --lib --bins` — ACP session tests must pass. -5. Verify no tokio type imports from `agent_client_protocol` or `agent_client_protocol_tokio`. -6. Update the capability matrix in `specs/045-interop-protocol-gaps/spec.md` accordingly. -7. Update `/agent.json` `protocol` field from `"acp/0.12.0"` to `"acp/0.13.2"`. +### Version Upgrade Note (0.12.1 → 0.14.0, completed in this PR) + +1. Review Breaking Changes Resolution table (SDK 0.12.1 → 0.14.0) above. +2. Workspace: `agent-client-protocol = "0.14.0"`, `agent-client-protocol-schema = "=0.13.6"`; delete `agent-client-protocol-tokio`. +3. Crate `Cargo.toml`: tombstone degated features as `= []`; fix `unstable-session-usage` → `["agent-client-protocol/unstable_end_turn_token_usage"]`; add core passthrough to `unstable-elicitation`. +4. Delete `handlers/set_session_model.rs`; remove all `session/set_model` handler code and tests. +5. Remove all inbound message-id plumbing; `message_ids_enabled` config field retained as no-op. +6. Rename `SetProvidersRequest/Response` and `DisableProvidersRequest/Response` to singular. +7. Degate all cfg sites for delete/logout/resume/add-dirs. +8. Build: `cargo check -p zeph-acp --features full`; `cargo nextest run -p zeph-acp --all-features`. +9. Live round-trip test: session/new → prompt → set_config_option{model} → set_mode → session/delete → logout. +10. Update `/agent.json` `protocol` field to `"acp/0.13.6"`. diff --git a/specs/README.md b/specs/README.md index f05c90b0a..7b6519141 100644 --- a/specs/README.md +++ b/specs/README.md @@ -95,7 +95,7 @@ Spec IDs (001–065) follow a logical grouping: | `011-tui/spec.md` | ratatui dashboard, spinner rule for background operations, TuiChannel, RenderCache, embed backfill progress, multi-session `SessionRegistry`, `/session` commands, compact paste indicator; Fleet panel (`f` key, #3884); reasoning token tracking; terminal title (#4354); fleet session lifecycle wiring (#4363) | `zeph-tui` | | `012-graph-memory/spec.md` | Entity graph, BFS recall, community detection, MAGMA typed edges, SYNAPSE spreading activation | `zeph-memory` | | `004-memory/004-6-graph-memory.md` | Graph memory sub-spec (concise reference within 004-memory): MAGMA typed edges, SYNAPSE config, A-MEM link weights, key invariants | `zeph-memory` | -| `013-acp/spec.md` | ACP transports, session management, permissions, fork/resume, session/close handlers, capability advertisement, /agent.json endpoint | `zeph-acp` | +| `013-acp/spec.md` | ACP transports, session management, permissions, fork/resume, session/close handlers, capability advertisement, /agent.json endpoint; 0.14.0 bump: session/set_model removed, message-id echo removed, provider renames, feature flag stabilizations | `zeph-acp` | | `014-a2a/spec.md` | A2A protocol, agent discovery, JSON-RPC 2.0, IBCT (Invocation-Bound Capability Tokens), HMAC-SHA256 signatures | `zeph-a2a` | | `015-self-learning/spec.md` | FeedbackDetector (multi-language), Wilson score, trust model, SAGE RL cross-session reward, ARISE trace improvement, STEM pattern-to-skill migration, ERL experiential learning | `zeph-skills` | | `016-agent-feedback/spec.md` | Implicit correction detection: `FeedbackDetector` (regex-only, 7 languages, dual anchoring tiers), `JudgeDetector` (LLM judge with adaptive thresholds, sliding-window rate limiter 5/min), four correction kinds (explicit rejection, alternative request, repetition, self-correction), CJK limitations | `zeph-agent-feedback` |