Skip to content

Commit 902e5fe

Browse files
authored
Merge pull request #90 from nikomatsakis/main
feat(sacp)!: use AsyncFnMut for tool closures with macro workaround
2 parents 641dbaf + 09d91e7 commit 902e5fe

File tree

17 files changed

+195
-116
lines changed

17 files changed

+195
-116
lines changed

src/sacp-conductor/src/conductor.rs

Lines changed: 76 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,15 @@ use futures::{
116116
SinkExt, StreamExt,
117117
channel::mpsc::{self},
118118
};
119-
use sacp::role::{ConductorToAgent, ConductorToClient, ConductorToProxy};
120119
use sacp::schema::{
121120
McpConnectRequest, McpConnectResponse, McpDisconnectNotification, McpOverAcpMessage,
122121
SuccessorMessage,
123122
};
124123
use sacp::{Agent, Client, Component, Error, JrMessage};
124+
use sacp::{
125+
ChainResponder, JrResponder, NullResponder,
126+
role::{ConductorToAgent, ConductorToClient, ConductorToProxy},
127+
};
125128
use sacp::{
126129
HasDefaultEndpoint, JrConnectionBuilder, JrConnectionCx, JrNotification, JrRequest,
127130
JrRequestCx, JrResponse, JrRole, MessageCx, UntypedMessage,
@@ -156,12 +159,12 @@ pub struct Conductor {
156159

157160
impl Conductor {
158161
pub fn new(
159-
name: String,
162+
name: impl ToString,
160163
component_list: impl ComponentList + 'static,
161164
mcp_bridge_mode: crate::McpBridgeMode,
162165
) -> Self {
163166
Conductor {
164-
name,
167+
name: name.to_string(),
165168
component_list: Box::new(component_list),
166169
mcp_bridge_mode,
167170
trace_writer: None,
@@ -193,11 +196,15 @@ impl Conductor {
193196

194197
pub fn into_connection_builder(
195198
self,
196-
) -> JrConnectionBuilder<ConductorMessageHandler, impl sacp::JrResponder<ConductorToClient>>
197-
{
198-
let (mut conductor_tx, mut conductor_rx) = mpsc::channel(128 /* chosen arbitrarily */);
199+
) -> JrConnectionBuilder<
200+
ConductorMessageHandler,
201+
ChainResponder<NullResponder, ConductorResponder>,
202+
> {
203+
let (conductor_tx, conductor_rx) = mpsc::channel(128 /* chosen arbitrarily */);
199204

200-
let mut state = ConductorHandlerState {
205+
let responder = ConductorResponder {
206+
conductor_rx,
207+
conductor_tx: conductor_tx.clone(),
201208
component_list: Some(self.component_list),
202209
bridge_listeners: Default::default(),
203210
bridge_connections: Default::default(),
@@ -208,23 +215,9 @@ impl Conductor {
208215
pending_requests: Default::default(),
209216
};
210217

211-
JrConnectionBuilder::new_with(ConductorMessageHandler {
212-
conductor_tx: conductor_tx.clone(),
213-
})
214-
.name(self.name)
215-
.with_spawned(async move |cx| {
216-
// Components are now spawned lazily in forward_initialize_request
217-
// when the first Initialize request is received.
218-
219-
// This is the "central actor" of the conductor. Most other things forward messages
220-
// via `conductor_tx` into this loop. This lets us serialize the conductor's activity.
221-
while let Some(message) = conductor_rx.next().await {
222-
state
223-
.handle_conductor_message(&cx, message, &mut conductor_tx)
224-
.await?;
225-
}
226-
Ok(())
227-
})
218+
JrConnectionBuilder::new_with(ConductorMessageHandler { conductor_tx })
219+
.name(self.name)
220+
.with_responder(responder)
228221
}
229222

230223
/// Convenience method to run the conductor with a transport.
@@ -254,43 +247,6 @@ pub struct ConductorMessageHandler {
254247
conductor_tx: mpsc::Sender<ConductorMessage>,
255248
}
256249

257-
/// The conductor manages the proxy chain lifecycle and message routing.
258-
///
259-
/// It maintains connections to all components in the chain and routes messages
260-
/// bidirectionally between the editor, components, and agent.
261-
///
262-
struct ConductorHandlerState {
263-
/// Manages the TCP listeners for MCP connections that will be proxied over ACP.
264-
bridge_listeners: McpBridgeListeners,
265-
266-
/// Manages active connections to MCP clients.
267-
bridge_connections: HashMap<String, McpBridgeConnection>,
268-
269-
/// The component list for lazy initialization.
270-
/// Set to None after components are instantiated.
271-
component_list: Option<Box<dyn ComponentList>>,
272-
273-
/// The chain of proxies before the agent (if any).
274-
///
275-
/// Populated lazily when the first Initialize request is received.
276-
proxies: Vec<JrConnectionCx<ConductorToProxy>>,
277-
278-
/// If the conductor is operating in agent mode, this will be the agent.
279-
/// If the conductor is operating in proxy mode, this will be None.
280-
///
281-
/// Populated lazily when the first Initialize request is received.
282-
agent: Option<JrConnectionCx<ConductorToAgent>>,
283-
284-
/// Mode for the MCP bridge (determines how to spawn bridge processes).
285-
mcp_bridge_mode: crate::McpBridgeMode,
286-
287-
/// Optional trace writer for sequence diagram visualization.
288-
trace_writer: Option<crate::trace::TraceWriter>,
289-
290-
/// Tracks pending requests for response tracing: id -> (from, to)
291-
pending_requests: HashMap<String, (String, String)>,
292-
}
293-
294250
impl JrMessageHandler for ConductorMessageHandler {
295251
type Role = ConductorToClient;
296252

@@ -336,7 +292,65 @@ impl JrMessageHandler for ConductorMessageHandler {
336292
}
337293
}
338294

339-
impl ConductorHandlerState {
295+
/// The conductor manages the proxy chain lifecycle and message routing.
296+
///
297+
/// It maintains connections to all components in the chain and routes messages
298+
/// bidirectionally between the editor, components, and agent.
299+
///
300+
pub struct ConductorResponder {
301+
conductor_rx: mpsc::Receiver<ConductorMessage>,
302+
303+
conductor_tx: mpsc::Sender<ConductorMessage>,
304+
305+
/// Manages the TCP listeners for MCP connections that will be proxied over ACP.
306+
bridge_listeners: McpBridgeListeners,
307+
308+
/// Manages active connections to MCP clients.
309+
bridge_connections: HashMap<String, McpBridgeConnection>,
310+
311+
/// The component list for lazy initialization.
312+
/// Set to None after components are instantiated.
313+
component_list: Option<Box<dyn ComponentList>>,
314+
315+
/// The chain of proxies before the agent (if any).
316+
///
317+
/// Populated lazily when the first Initialize request is received.
318+
proxies: Vec<JrConnectionCx<ConductorToProxy>>,
319+
320+
/// If the conductor is operating in agent mode, this will be the agent.
321+
/// If the conductor is operating in proxy mode, this will be None.
322+
///
323+
/// Populated lazily when the first Initialize request is received.
324+
agent: Option<JrConnectionCx<ConductorToAgent>>,
325+
326+
/// Mode for the MCP bridge (determines how to spawn bridge processes).
327+
mcp_bridge_mode: crate::McpBridgeMode,
328+
329+
/// Optional trace writer for sequence diagram visualization.
330+
trace_writer: Option<crate::trace::TraceWriter>,
331+
332+
/// Tracks pending requests for response tracing: id -> (from, to)
333+
pending_requests: HashMap<String, (String, String)>,
334+
}
335+
336+
impl JrResponder<ConductorToClient> for ConductorResponder {
337+
async fn run(mut self, cx: JrConnectionCx<ConductorToClient>) -> Result<(), sacp::Error> {
338+
// Components are now spawned lazily in forward_initialize_request
339+
// when the first Initialize request is received.
340+
341+
let mut conductor_tx = self.conductor_tx.clone();
342+
343+
// This is the "central actor" of the conductor. Most other things forward messages
344+
// via `conductor_tx` into this loop. This lets us serialize the conductor's activity.
345+
while let Some(message) = self.conductor_rx.next().await {
346+
self.handle_conductor_message(&cx, message, &mut conductor_tx)
347+
.await?;
348+
}
349+
Ok(())
350+
}
351+
}
352+
353+
impl ConductorResponder {
340354
/// Convert a component index to a trace-friendly name.
341355
fn component_name(&self, index: usize) -> String {
342356
if self.is_agent_component(index) {

src/sacp-conductor/tests/mcp_integration/proxy.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,15 @@ impl Component for ProxyComponent {
2626
async fn serve(self, client: impl Component) -> Result<(), sacp::Error> {
2727
let test_server = McpServer::builder("test")
2828
.instructions("A simple test MCP server with an echo tool")
29-
.tool_fn(
29+
.tool_fn_mut(
3030
"echo",
3131
"Echoes back the input message",
3232
async |params: EchoParams, _context| {
3333
Ok(EchoOutput {
3434
result: format!("Echo: {}", params.message),
3535
})
3636
},
37+
sacp::tool_fn_mut!(),
3738
)
3839
.build();
3940

src/sacp-conductor/tests/mcp_server_handler_chain.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,15 @@ impl Component for ProxyWithMcpAndHandler {
7474
// Create an MCP server with a simple tool
7575
let mcp_server = McpServer::builder("test-server".to_string())
7676
.instructions("A test MCP server")
77-
.tool_fn(
77+
.tool_fn_mut(
7878
"echo",
7979
"Echoes back the input",
8080
async |params: EchoParams, _cx| {
8181
Ok(EchoOutput {
8282
result: format!("Echo: {}", params.message),
8383
})
8484
},
85+
sacp::tool_fn_mut!(),
8586
)
8687
.build();
8788

src/sacp-conductor/tests/scoped_mcp_server.rs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
77
use elizacp::ElizaAgent;
88
use sacp::mcp_server::McpServer;
9-
use sacp::{Agent, ClientToAgent, Component, DynComponent, HasEndpoint, JrRole, ProxyToConductor};
9+
use sacp::{
10+
Agent, ClientToAgent, Component, DynComponent, HasEndpoint, JrResponder, JrRole,
11+
ProxyToConductor,
12+
};
1013
use sacp_conductor::{Conductor, McpBridgeMode};
1114
use schemars::JsonSchema;
1215
use serde::{Deserialize, Serialize};
@@ -83,9 +86,9 @@ async fn test_scoped_mcp_server_through_session() -> Result<(), sacp::Error> {
8386

8487
struct ScopedProxy;
8588

86-
fn make_mcp_server<Role: JrRole>(
87-
values: &Mutex<Vec<String>>,
88-
) -> McpServer<Role, impl sacp::JrResponder<Role> + use<'_, Role>>
89+
fn make_mcp_server<'a, Role: JrRole>(
90+
values: &'a Mutex<Vec<String>>,
91+
) -> McpServer<Role, impl JrResponder<Role>>
8992
where
9093
Role: HasEndpoint<Agent>,
9194
{
@@ -96,19 +99,25 @@ where
9699

97100
McpServer::builder("test".to_string())
98101
.instructions("A test MCP server with scoped tool")
99-
.tool_fn(
102+
.tool_fn_mut(
100103
"push",
101104
"Push a value to the collected values",
102105
async |input: PushInput, _cx| {
103106
let mut values = values.lock().expect("not poisoned");
104107
values.extend(input.elements);
105108
Ok(values.len())
106109
},
110+
sacp::tool_fn_mut!(),
111+
)
112+
.tool_fn_mut(
113+
"get",
114+
"Get the collected values",
115+
async |(): (), _cx| {
116+
let values = values.lock().expect("not poisoned");
117+
Ok(values.clone())
118+
},
119+
sacp::tool_fn_mut!(),
107120
)
108-
.tool_fn("get", "Get the collected values", async |(): (), _cx| {
109-
let values = values.lock().expect("not poisoned");
110-
Ok(values.clone())
111-
})
112121
.build()
113122
}
114123

src/sacp-conductor/tests/test_session_id_in_mcp_tools.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,15 @@ fn create_echo_proxy() -> Result<sacp::DynComponent, sacp::Error> {
3232
// Create MCP server with an echo tool that returns the session_id
3333
let mcp_server = McpServer::builder("echo_server".to_string())
3434
.instructions("Test MCP server with session_id echo tool")
35-
.tool_fn(
35+
.tool_fn_mut(
3636
"echo",
3737
"Returns the current session_id",
3838
async |_input: EchoInput, context| {
3939
Ok(EchoOutput {
4040
acp_url: context.acp_url(),
4141
})
4242
},
43+
sacp::tool_fn_mut!(),
4344
)
4445
.build();
4546

@@ -51,7 +52,7 @@ struct ProxyWithEchoServer<R: sacp::JrResponder<ProxyToConductor>> {
5152
mcp_server: McpServer<ProxyToConductor, R>,
5253
}
5354

54-
impl<R: sacp::JrResponder<ProxyToConductor> + 'static> Component for ProxyWithEchoServer<R> {
55+
impl<R: sacp::JrResponder<ProxyToConductor> + 'static + Send> Component for ProxyWithEchoServer<R> {
5556
async fn serve(self, client: impl Component) -> Result<(), sacp::Error> {
5657
ProxyToConductor::builder()
5758
.name("echo-proxy")

src/sacp-derive/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "sacp-derive"
33
version = "8.0.0"
4-
edition = "2021"
4+
edition = "2024"
55
description = "Derive macros for SACP JSON-RPC traits"
66
license = "MIT OR Apache-2.0"
77
repository = "https://github.com/anthropics/acp"

src/sacp-tokio/src/acp_agent.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,25 @@ impl AcpAgent {
102102
}
103103
}
104104

105+
/// Create an ACP agent for Zed Industries' Claude Code tool.
106+
/// Just runs `npx -y @zed-industries/claude-code-acp@latest`.
107+
pub fn zed_claude_code() -> Self {
108+
Self::from_str("npx -y @zed-industries/claude-code-acp@latest").expect("valid bash command")
109+
}
110+
111+
/// Create an ACP agent for Zed Industries' Codex tool.
112+
/// Just runs `npx -y @zed-industries/codex-acp@latest`.
113+
pub fn zed_codex() -> Self {
114+
Self::from_str("npx -y @zed-industries/codex-acp@latest").expect("valid bash command")
115+
}
116+
117+
/// Create an ACP agent for Google's Gemini CLI.
118+
/// Just runs `npx -y -- @google/gemini-cli@latest --experimental-acp`.
119+
pub fn google_gemini() -> Self {
120+
Self::from_str("npx -y -- @google/gemini-cli@latest --experimental-acp")
121+
.expect("valid bash command")
122+
}
123+
105124
/// Get the underlying [`sacp::schema::McpServer`] configuration.
106125
pub fn server(&self) -> &sacp::schema::McpServer {
107126
&self.server

src/sacp/src/jsonrpc.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1390,7 +1390,7 @@ impl<T> IntoHandled<T> for Handled<T> {
13901390
/// See the [Event Loop and Concurrency](JrConnection#event-loop-and-concurrency) section
13911391
/// for more details.
13921392
#[derive(Clone, Debug)]
1393-
pub struct JrConnectionCx<Role: JrRole> {
1393+
pub struct JrConnectionCx<Role> {
13941394
#[expect(dead_code)]
13951395
role: Role,
13961396
message_tx: OutgoingMessageTx,

src/sacp/src/jsonrpc/dynamic_handler.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::role::JrRole;
55
use crate::{Handled, JrConnectionCx, MessageCx, jsonrpc::JrMessageHandlerSend};
66

77
/// Internal dyn-safe wrapper around `JrMessageHandlerSend`
8-
pub(crate) trait DynamicHandler<Role: JrRole>: Send {
8+
pub(crate) trait DynamicHandler<Role>: Send {
99
fn dyn_handle_message(
1010
&mut self,
1111
message: MessageCx,
@@ -30,7 +30,7 @@ impl<H: JrMessageHandlerSend> DynamicHandler<H::Role> for H {
3030
}
3131

3232
/// Messages used to add/remove dynamic handlers
33-
pub(crate) enum DynamicHandlerMessage<Role: JrRole> {
33+
pub(crate) enum DynamicHandlerMessage<Role> {
3434
AddDynamicHandler(Uuid, Box<dyn DynamicHandler<Role>>),
3535
RemoveDynamicHandler(Uuid),
3636
}

src/sacp/src/jsonrpc/responder.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ use crate::{JrConnectionCx, role::JrRole};
1414
/// when the connection is active.
1515
pub trait JrResponder<Role: JrRole>: Send {
1616
/// Run this responder to completion.
17-
fn run(self, cx: JrConnectionCx<Role>)
18-
-> impl Future<Output = Result<(), crate::Error>> + Send;
17+
fn run(self, cx: JrConnectionCx<Role>) -> impl Future<Output = Result<(), crate::Error>> + Send;
1918
}
2019

2120
/// A no-op responder that completes immediately.

0 commit comments

Comments
 (0)