Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 10 additions & 18 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ tracing-subscriber = { version = "0.3.23", features = ["env-filter", "json"] }
tracing-appender = "0.2"

# ACP
agent-client-protocol = "0.14.0"
agent-client-protocol = "2.0.0"

# MCP and API clients
rmcp = { version = "^1.7.0", default-features = false }
Expand Down
4 changes: 2 additions & 2 deletions crates/acp-utils/src/client/event.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use acp::Error;
use acp::Responder;
use acp::schema::{SessionUpdate, StopReason};
use acp::schema::v1::{SessionUpdate, StopReason};
use agent_client_protocol as acp;
use agent_client_protocol::schema::{SessionConfigOption, SessionId, SessionInfo};
use agent_client_protocol::schema::v1::{SessionConfigOption, SessionId, SessionInfo};

use crate::notifications::{
AuthMethodsUpdatedParams, ContextClearedParams, ContextCompactionParams, ContextUsageParams, ElicitationParams,
Expand Down
4 changes: 2 additions & 2 deletions crates/acp-utils/src/client/prompt_handle.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use agent_client_protocol::schema::{ContentBlock, SessionId};
use agent_client_protocol::schema::v1::{ContentBlock, SessionId};
use std::path::{Path, PathBuf};
use tokio::sync::mpsc;

Expand Down Expand Up @@ -118,7 +118,7 @@ impl AcpPromptHandle {

#[cfg(test)]
mod tests {
use agent_client_protocol::schema::TextContent;
use agent_client_protocol::schema::v1::TextContent;

use super::*;

Expand Down
4 changes: 2 additions & 2 deletions crates/acp-utils/src/client/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::notifications::{
AuthMethodsUpdatedParams, ContextClearedParams, ContextCompactionParams, ContextUsageParams, ElicitationParams,
McpNotification, McpRequest, SubAgentProgressParams,
};
use agent_client_protocol::schema::{
use agent_client_protocol::schema::v1::{
AuthMethod, AuthenticateRequest, CancelNotification, ConfigOptionUpdate, ContentBlock, InitializeRequest,
InitializeResponse, ListSessionsRequest, LoadSessionRequest, NewSessionRequest, NewSessionResponse,
PermissionOptionId, PermissionOptionKind, PromptCapabilities, PromptRequest, RequestPermissionOutcome,
Expand Down Expand Up @@ -274,7 +274,7 @@ async fn handle_command(
}
}
PromptCommand::SetConfigOption { session_id, config_id, value } => {
let req = SetSessionConfigOptionRequest::new(session_id.clone(), config_id, value);
let req = SetSessionConfigOptionRequest::new(session_id.clone(), config_id, value.as_str());
spawn_request_to_event(
cx,
event_tx,
Expand Down
35 changes: 13 additions & 22 deletions crates/acp-utils/src/client/tokio_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
//! `async_process::Command`, which wraps stdio in `blocking::Unblock`. Inside a
//! tokio runtime that causes a busy loop. This avoids the issue by spawning stdio agents with `tokio::process::Command`
//!
use agent_client_protocol::schema::{McpServer, McpServerStdio};
use agent_client_protocol::util::internal_error;
use agent_client_protocol::{AcpAgent, ByteStreams, ConnectTo, Error, Role, util};
use agent_client_protocol::{AcpAgent, AcpAgentConfig, ByteStreams, ConnectTo, Error, Role, util};
use std::path::PathBuf;
use std::process::Stdio;
use std::str::FromStr;
Expand All @@ -16,47 +15,38 @@ use tokio::sync::oneshot;
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};

pub struct TokioAcpAgent {
stdio: McpServerStdio,
config: AcpAgentConfig,
}

impl TokioAcpAgent {
pub fn from_command(command: impl Into<PathBuf>, args: Vec<String>) -> Self {
let command = command.into();
let name = command.file_name().and_then(|name| name.to_str()).unwrap_or("acp-agent").to_string();
let mut stdio = McpServerStdio::new(name, command);
stdio.args = args;
Self { stdio }
Self { config: AcpAgentConfig::new(command).args(args) }
}

pub fn stdio(&self) -> &McpServerStdio {
&self.stdio
pub fn config(&self) -> &AcpAgentConfig {
&self.config
}
}

impl<T: Role> ConnectTo<T> for TokioAcpAgent {
async fn connect_to(self, client: impl ConnectTo<T::Counterpart>) -> Result<(), Error> {
connect_stdio::<T>(self.stdio, client).await
connect_stdio::<T>(self.config, client).await
}
}

impl FromStr for TokioAcpAgent {
type Err = Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match AcpAgent::from_str(s)?.into_server() {
McpServer::Stdio(stdio) => Ok(Self { stdio }),
_ => Err(util::internal_error("unsupported ACP agent transport")),
}
Ok(Self { config: AcpAgent::from_str(s)?.into_config() })
}
}

async fn connect_stdio<T: Role>(server: McpServerStdio, client: impl ConnectTo<T::Counterpart>) -> Result<(), Error> {
async fn connect_stdio<T: Role>(config: AcpAgentConfig, client: impl ConnectTo<T::Counterpart>) -> Result<(), Error> {
let (stdin, stdout, stderr, mut child) = {
let mut cmd = Command::new(&server.command);
cmd.args(&server.args);
for env_var in &server.env {
cmd.env(&env_var.name, &env_var.value);
}
let mut cmd = Command::new(config.command());
cmd.args(config.arguments());
cmd.envs(config.environment());

let mut child = cmd
.stdin(Stdio::piped())
Expand Down Expand Up @@ -98,7 +88,8 @@ async fn connect_stdio<T: Role>(server: McpServerStdio, client: impl ConnectTo<T

let bytes = ByteStreams::new(stdin.compat_write(), stdout.compat());
tokio::select! {
result = ConnectTo::<T>::connect_to(bytes, client) => result,
biased;
result = child_fut => result,
result = ConnectTo::<T>::connect_to(bytes, client) => result,
}
}
2 changes: 1 addition & 1 deletion crates/acp-utils/src/content.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use agent_client_protocol::schema as acp;
use agent_client_protocol::schema::v1 as acp;

/// Converts ACP `ContentBlock` to plain text.
///
Expand Down
18 changes: 9 additions & 9 deletions crates/acp-utils/src/notifications.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//! notifications.
use std::path::PathBuf;

use agent_client_protocol::schema::AuthMethod;
use agent_client_protocol::schema::v1::{AuthMethod, Meta};
use agent_client_protocol::{JsonRpcNotification, JsonRpcRequest, JsonRpcResponse};
pub use mcp_utils::display_meta::{ToolDisplayMeta, ToolResultMeta};
pub use rmcp::model::CreateElicitationRequestParams;
Expand Down Expand Up @@ -234,12 +234,12 @@ impl SessionDisplayMeta {
}

#[must_use]
pub fn to_meta(&self) -> agent_client_protocol::schema::Meta {
pub fn to_meta(&self) -> Meta {
to_aether_meta(self)
}

#[must_use]
pub fn from_meta(meta: Option<&agent_client_protocol::schema::Meta>) -> Self {
pub fn from_meta(meta: Option<&Meta>) -> Self {
from_aether_meta(meta)
}
}
Expand All @@ -257,23 +257,23 @@ pub struct AetherCapabilities {

impl AetherCapabilities {
#[must_use]
pub fn to_meta(self) -> agent_client_protocol::schema::Meta {
pub fn to_meta(self) -> Meta {
to_aether_meta(&self)
}

#[must_use]
pub fn from_meta(meta: Option<&agent_client_protocol::schema::Meta>) -> Self {
pub fn from_meta(meta: Option<&Meta>) -> Self {
from_aether_meta(meta)
}
}

fn to_aether_meta<T: Serialize>(value: &T) -> agent_client_protocol::schema::Meta {
let mut meta = agent_client_protocol::schema::Meta::new();
fn to_aether_meta<T: Serialize>(value: &T) -> Meta {
let mut meta = Meta::new();
meta.insert(AETHER_META_NAMESPACE.to_string(), serde_json::json!(value));
meta
}

fn from_aether_meta<T: DeserializeOwned + Default>(meta: Option<&agent_client_protocol::schema::Meta>) -> T {
fn from_aether_meta<T: DeserializeOwned + Default>(meta: Option<&Meta>) -> T {
meta.and_then(|m| m.get(AETHER_META_NAMESPACE))
.cloned()
.and_then(|value| serde_json::from_value(value).ok())
Expand Down Expand Up @@ -360,7 +360,7 @@ pub struct SubAgentToolError {
#[cfg(test)]
mod tests {
use agent_client_protocol::JsonRpcMessage;
use agent_client_protocol::schema::AuthMethodAgent;
use agent_client_protocol::schema::v1::AuthMethodAgent;

use super::*;

Expand Down
2 changes: 1 addition & 1 deletion crates/acp-utils/src/testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//! and returns a receiver that resolves when the responder is consumed.

use crate::notifications::{ElicitationParams, ElicitationResponse, McpNotification};
use agent_client_protocol::schema::SessionNotification;
use agent_client_protocol::schema::v1::SessionNotification;
use agent_client_protocol::{
self as acp, Agent, Builder, ByteStreams, Client, ConnectionTo, HandleDispatchFrom, NullRun, Responder,
};
Expand Down
5 changes: 3 additions & 2 deletions crates/acp-utils/tests/client_session.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use acp_utils::client::spawn_acp_session;
use acp_utils::testing::duplex_pair;
use agent_client_protocol::schema::{
use agent_client_protocol::schema::ProtocolVersion;
use agent_client_protocol::schema::v1::{
CancelNotification, Implementation, InitializeRequest, InitializeResponse, NewSessionRequest, NewSessionResponse,
PromptRequest, ProtocolVersion, SessionId, SetSessionConfigOptionRequest,
PromptRequest, SessionId, SetSessionConfigOptionRequest,
};
use agent_client_protocol::{self as acp, Agent};
use std::path::PathBuf;
Expand Down
36 changes: 17 additions & 19 deletions crates/acp-utils/tests/tokio_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,39 @@ use tokio::task::LocalSet;
#[test]
fn parses_shell_command() {
let agent = TokioAcpAgent::from_str("aether acp --foo bar").expect("parses");
let server = agent.stdio();
assert_eq!(server.command.as_path(), Path::new("aether"));
assert_eq!(server.args.as_slice(), &["acp", "--foo", "bar"]);
let config = agent.config();
assert_eq!(config.command(), Path::new("aether"));
assert_eq!(config.arguments(), ["acp", "--foo", "bar"]);
}

#[test]
fn parses_quoted_shell_command() {
let agent = TokioAcpAgent::from_str(r#"python "my agent.py" --name "Test Agent""#).expect("parses");
let server = agent.stdio();
assert_eq!(server.command.as_path(), Path::new("python"));
assert_eq!(server.args.as_slice(), &["my agent.py", "--name", "Test Agent"]);
let config = agent.config();
assert_eq!(config.command(), Path::new("python"));
assert_eq!(config.arguments(), ["my agent.py", "--name", "Test Agent"]);
}

#[test]
fn parses_leading_environment_variables() {
let agent = TokioAcpAgent::from_str("RUST_LOG=debug aether acp").expect("parses");
let server = agent.stdio();
assert_eq!(server.command.as_path(), Path::new("aether"));
assert_eq!(server.args.as_slice(), &["acp"]);
assert_eq!(server.env[0].name, "RUST_LOG");
assert_eq!(server.env[0].value, "debug");
let config = agent.config();
assert_eq!(config.command(), Path::new("aether"));
assert_eq!(config.arguments(), ["acp"]);
assert_eq!(config.environment().get("RUST_LOG"), Some(&"debug".to_string()));
}

#[test]
fn parses_json_stdio_agent_config() {
let agent = TokioAcpAgent::from_str(
r#"{"type":"stdio","name":"test-agent","command":"/usr/bin/python","args":["agent.py","--verbose"],"env":[{"name":"RUST_LOG","value":"debug"}]}"#,
r#"{"command":"/usr/bin/python","args":["agent.py","--verbose"],"env":{"RUST_LOG":"debug"}}"#,
)
.expect("parses");

let server = agent.stdio();
assert_eq!(server.command.as_path(), Path::new("/usr/bin/python"));
assert_eq!(server.args.as_slice(), &["agent.py", "--verbose"]);
assert_eq!(server.env[0].name, "RUST_LOG");
assert_eq!(server.env[0].value, "debug");
let config = agent.config();
assert_eq!(config.command(), Path::new("/usr/bin/python"));
assert_eq!(config.arguments(), ["agent.py", "--verbose"]);
assert_eq!(config.environment().get("RUST_LOG"), Some(&"debug".to_string()));
}

#[test]
Expand All @@ -51,8 +49,8 @@ fn rejects_empty_command() {
}

#[test]
fn rejects_non_stdio_transport_at_parse_time() {
let json = r#"{"type":"http","name":"remote","url":"https://example.com/agent","headers":[]}"#;
fn rejects_invalid_agent_config() {
let json = r#"{"url":"https://example.com/agent"}"#;
assert!(TokioAcpAgent::from_str(json).is_err());
}

Expand Down
2 changes: 1 addition & 1 deletion crates/aether-cli/src/acp/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use super::state::AcpState;
use acp_utils::notifications::{
McpRequest, PromptSearchParams, SessionPreviewParams, WorkspaceListParams, WorkspaceMoveParams,
};
use agent_client_protocol::schema::{
use agent_client_protocol::schema::v1::{
AuthenticateRequest, CancelNotification, InitializeRequest, ListSessionsRequest, LoadSessionRequest,
NewSessionRequest, PromptRequest, SetSessionConfigOptionRequest,
};
Expand Down
4 changes: 2 additions & 2 deletions crates/aether-cli/src/acp/model_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use acp_utils::config_meta::{ConfigOptionMeta, SelectOptionMeta};
use acp_utils::config_option_id::ConfigOptionId;
use aether_auth::OAuthCredentialStorage;
use aether_core::agent_spec::AgentSpec;
use agent_client_protocol::schema::{self as acp, SessionConfigOption, SessionConfigOptionCategory};
use agent_client_protocol::schema::v1::{self as acp, SessionConfigOption, SessionConfigOptionCategory};
use llm::ReasoningEffort;
use llm::catalog::{LlmModel, ModelSpec};
use std::collections::{BTreeMap, HashSet};
Expand Down Expand Up @@ -252,7 +252,7 @@ mod tests {
use super::*;
use aether_auth::FakeOAuthCredentialStore;
use aether_core::agent_spec::{AgentSpecExposure, ToolFilter};
use agent_client_protocol::schema::{SessionConfigKind, SessionConfigSelectOption, SessionConfigSelectOptions};
use agent_client_protocol::schema::v1::{SessionConfigKind, SessionConfigSelectOption, SessionConfigSelectOptions};
use llm::catalog::{AnthropicModel, BedrockFoundationModel, BedrockModel, DeepSeekModel, GeminiModel};

fn test_models() -> Vec<LlmModel> {
Expand Down
2 changes: 1 addition & 1 deletion crates/aether-cli/src/acp/protocol/commands.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use agent_client_protocol::schema as acp;
use agent_client_protocol::schema::v1 as acp;
use rmcp::model::Prompt as McpPrompt;

/// Converts an MCP Prompt to an ACP `AvailableCommand`
Expand Down
2 changes: 1 addition & 1 deletion crates/aether-cli/src/acp/protocol/content.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use acp_utils::content::format_embedded_resource;
use agent_client_protocol::schema::{self as acp, ContentBlock, TextContent};
use agent_client_protocol::schema::v1::{self as acp, ContentBlock, TextContent};
use llm::ContentBlock as LlmContentBlock;

/// Convert client-supplied ACP content blocks into LLM content blocks for the
Expand Down
Loading