@@ -4,140 +4,273 @@ This chapter explains how to build an ACP agent using the `sacp` crate.
44
55## Overview
66
7- An agent is the final component in a SACP proxy chain . It provides the base AI model behavior and doesn't need awareness of SACP - it's just a standard ACP agent .
7+ An agent is the component that provides AI model behavior in an ACP system . It receives prompts from clients (editors like Zed or Claude Code) and returns responses. Agents can also work as the final component in a conductor proxy chain .
88
9- However, the ` sacp ` crate provides useful types and utilities for building ACP agents.
9+ ## Quick Start
1010
11- ## Core Types
11+ Here's a minimal agent that handles initialization:
1212
13- The ` sacp ` crate provides Rust types for ACP protocol messages:
13+ ``` rust
14+ use sacp :: AgentToClient ;
15+ use sacp :: schema :: {AgentCapabilities , InitializeRequest , InitializeResponse };
16+ use tokio_util :: compat :: {TokioAsyncReadCompatExt , TokioAsyncWriteCompatExt };
17+
18+ #[tokio:: main]
19+ async fn main () -> Result <(), sacp :: Error > {
20+ AgentToClient :: builder ()
21+ . name (" my-agent" )
22+ . on_receive_request (
23+ async move | req : InitializeRequest , request_cx , _cx | {
24+ request_cx . respond (InitializeResponse {
25+ protocol_version : req . protocol_version,
26+ agent_capabilities : AgentCapabilities :: default (),
27+ auth_methods : Default :: default (),
28+ agent_info : Default :: default (),
29+ meta : Default :: default (),
30+ })
31+ },
32+ sacp :: on_receive_request! (),
33+ )
34+ . serve (sacp :: ByteStreams :: new (
35+ tokio :: io :: stdout (). compat_write (),
36+ tokio :: io :: stdin (). compat (),
37+ ))
38+ . await
39+ }
40+ ```
41+
42+ ## Core Concepts
43+
44+ ### The Builder Pattern
45+
46+ Agents are built using ` AgentToClient::builder() ` . You register handlers for different message types, then call ` .serve() ` with a transport:
1447
1548``` rust
16- use sacp :: {
17- InitializeRequest , InitializeResponse ,
18- PromptRequest , PromptResponse ,
19- // ... other ACP types
20- };
49+ AgentToClient :: builder ()
50+ . name (" my-agent" ) // For logging/debugging
51+ . on_receive_request (handler1 , macro1 ) // Handle specific request type
52+ . on_receive_request (handler2 , macro2 ) // Chain multiple handlers
53+ . on_receive_notification (handler3 , macro3 ) // Handle notifications
54+ . serve (transport )
55+ . await
2156```
2257
23- These types handle:
24- - Serialization/deserialization
25- - Protocol validation
26- - Type safety for message handling
58+ ### Request Handlers
2759
28- ## JSON-RPC Foundation
60+ Request handlers receive three parameters:
2961
30- The ` sacp ` crate includes a JSON-RPC layer that handles:
62+ 1 . ** The request** - Typed by what you're handling (e.g., ` InitializeRequest ` , ` PromptRequest ` )
63+ 2 . ** The request context** (` JrRequestCx ` ) - Used to send the response
64+ 3 . ** The connection context** (` JrConnectionCx ` ) - Used for sending notifications or spawning tasks
3165
32- - Message framing over stdio or other transports
33- - Request/response correlation
34- - Notification handling
35- - Error propagation
66+ ``` rust
67+ . on_receive_request (
68+ async move | request : PromptRequest , request_cx , cx | {
69+ // Process the request...
70+
71+ // Send the response
72+ request_cx . respond (PromptResponse {
73+ stop_reason : StopReason :: EndTurn ,
74+ meta : None ,
75+ })
76+ },
77+ sacp :: on_receive_request! (),
78+ )
79+ ```
80+
81+ ### The Witness Macro
82+
83+ Every handler registration requires a "witness" macro as the final parameter. This is a workaround for missing Rust language features (return-type notation). Always use the matching macro:
84+
85+ - ` .on_receive_request(..., sacp::on_receive_request!()) `
86+ - ` .on_receive_notification(..., sacp::on_receive_notification!()) `
87+ - ` .on_receive_message(..., sacp::on_receive_message!()) `
88+
89+ ### Sending Notifications
90+
91+ Use the connection context to send notifications back to the client:
3692
3793``` rust
38- use sacp :: {JsonRpcConnection , JsonRpcHandler };
94+ . on_receive_request (
95+ async move | request : PromptRequest , request_cx , cx | {
96+ // Send streaming content
97+ cx . send_notification (SessionNotification {
98+ session_id : request . session_id. clone (),
99+ update : SessionUpdate :: AgentMessageChunk (ContentChunk {
100+ content : " Hello!" . into (),
101+ meta : None ,
102+ }),
103+ meta : None ,
104+ })? ;
105+
106+ // Complete the request
107+ request_cx . respond (PromptResponse {
108+ stop_reason : StopReason :: EndTurn ,
109+ meta : None ,
110+ })
111+ },
112+ sacp :: on_receive_request! (),
113+ )
114+ ```
115+
116+ ### Spawning Background Work
39117
40- // Create a connection over stdio
41- let connection = JsonRpcConnection :: new (stdin (), stdout (), my_handler );
118+ Don't block the message loop with long-running operations. Use ` cx.spawn() ` to run work in the background:
42119
43- // Run the message loop
44- connection . run (). await ? ;
120+ ``` rust
121+ . on_receive_request (
122+ async move | request : PromptRequest , request_cx , cx | {
123+ let cx_clone = cx . clone ();
124+ cx . spawn (async move {
125+ // Long-running AI inference...
126+ let response = generate_response (& request ). await ;
127+
128+ // Send notification with result
129+ cx_clone . send_notification (SessionNotification { /* ... */ })? ;
130+
131+ // Complete the request
132+ request_cx . respond (PromptResponse {
133+ stop_reason : StopReason :: EndTurn ,
134+ meta : None ,
135+ })
136+ })
137+ },
138+ sacp :: on_receive_request! (),
139+ )
45140```
46141
47- ## Handler Pattern
142+ ## Building a Reusable Agent Component
48143
49- Implement ` JsonRpcHandler ` to process ACP messages :
144+ For agents that can be composed into larger systems (e.g., with the conductor), implement the ` Component ` trait :
50145
51146``` rust
52- use sacp :: {JsonRpcHandler , MessageAndCx , Handled };
147+ use sacp :: {AgentToClient , Component };
148+ use sacp :: schema :: * ;
53149
54- struct MyAgent {
55- // Agent state
150+ pub struct MyAgent {
151+ config : AgentConfig ,
56152}
57153
58- impl JsonRpcHandler for MyAgent {
59- async fn handle_message (& mut self , message : MessageAndCx ) -> Result <Handled > {
60- match message {
61- MessageAndCx :: Request (req , cx ) => {
62- match req {
63- AcpRequest :: Initialize (init ) => {
64- // Handle initialization
65- let response = InitializeResponse {
66- protocolVersion: " 0.7.0" ,
67- serverInfo: ServerInfo { /* ... */ },
68- capabilities : Capabilities { /* ... */ },
69- };
70- cx . respond (response )? ;
71- Ok (Handled :: FullyHandled )
72- }
73- AcpRequest :: Prompt (prompt ) => {
74- // Call your AI model
75- let response = self . generate_response (prompt ). await ? ;
76- cx . respond (response )? ;
77- Ok (Handled :: FullyHandled )
154+ impl Component for MyAgent {
155+ async fn serve (self , client : impl Component ) -> Result <(), sacp :: Error > {
156+ AgentToClient :: builder ()
157+ . name (" my-agent" )
158+ . on_receive_request (
159+ async | req : InitializeRequest , request_cx , _cx | {
160+ request_cx . respond (InitializeResponse {
161+ protocol_version : req . protocol_version,
162+ agent_capabilities : AgentCapabilities :: default (),
163+ auth_methods : Default :: default (),
164+ agent_info : Default :: default (),
165+ meta : Default :: default (),
166+ })
167+ },
168+ sacp :: on_receive_request! (),
169+ )
170+ . on_receive_request (
171+ {
172+ let agent = self . clone ();
173+ async move | req : PromptRequest , request_cx , cx | {
174+ agent . handle_prompt (req , request_cx , cx ). await
78175 }
79- // ... other message types
80- }
81- }
82- MessageAndCx :: Notification (notif , cx ) => {
83- // Handle notifications
84- }
85- }
176+ },
177+ sacp :: on_receive_request! (),
178+ )
179+ . connect_to (client )?
180+ . serve ()
181+ . await
86182 }
87183}
88184```
89185
90- ## Working with Proxies
91-
92- Your agent doesn't need to know about SACP proxies. However, there are some optional capabilities that improve proxy integration:
186+ Note the difference: ` .serve(transport) ` for standalone agents vs ` .connect_to(client)?.serve() ` for composable components.
93187
94- ### MCP-over-ACP Support
188+ ## Handling Multiple Request Types
95189
96- If your agent can handle MCP servers declared with ` acp:UUID ` URLs, advertise the capability :
190+ Chain multiple ` .on_receive_request() ` calls to handle different message types. Handlers are tried in order until one matches :
97191
98192``` rust
99- InitializeResponse {
100- // ...
101- _meta : json! ({
102- " mcp_acp_transport" : true
103- }),
104- }
193+ AgentToClient :: builder ()
194+ . name (" my-agent" )
195+ . on_receive_request (
196+ async | req : InitializeRequest , request_cx , _cx | {
197+ request_cx . respond (InitializeResponse { /* ... */ })
198+ },
199+ sacp :: on_receive_request! (),
200+ )
201+ . on_receive_request (
202+ async | req : NewSessionRequest , request_cx , _cx | {
203+ request_cx . respond (NewSessionResponse { /* ... */ })
204+ },
205+ sacp :: on_receive_request! (),
206+ )
207+ . on_receive_request (
208+ async | req : PromptRequest , request_cx , cx | {
209+ // Handle prompts...
210+ request_cx . respond (PromptResponse { /* ... */ })
211+ },
212+ sacp :: on_receive_request! (),
213+ )
214+ . serve (transport )
215+ . await
105216```
106217
107- This allows the conductor to skip bridging and pass MCP declarations through directly.
218+ ## Catch-All Handler
108219
109- Without this capability, the conductor will automatically bridge MCP-over-ACP to stdio for you.
220+ Use ` .on_receive_message() ` to handle any message not caught by specific handlers:
110221
111- ## Testing
222+ ``` rust
223+ AgentToClient :: builder ()
224+ . name (" my-agent" )
225+ . on_receive_request (/* ... specific handlers ... */ )
226+ . on_receive_message (
227+ async move | message : MessageCx , cx | {
228+ // Return an error for unhandled messages
229+ message . respond_with_error (
230+ sacp :: util :: internal_error (" Unhandled message type" ),
231+ cx ,
232+ )
233+ },
234+ sacp :: on_receive_message! (),
235+ )
236+ . serve (transport )
237+ . await
238+ ```
239+
240+ ## Protocol Types
112241
113- The ` sacp ` crate provides test utilities :
242+ The ` sacp::schema ` module provides all ACP protocol types :
114243
115244``` rust
116- #[cfg(test)]
117- mod tests {
118- use sacp :: testing :: * ;
245+ use sacp :: schema :: {
246+ // Initialization
247+ InitializeRequest , InitializeResponse ,
248+ AgentCapabilities ,
119249
120- #[test]
121- fn test_prompt_handling () {
122- let agent = MyAgent :: new ();
123- let response = agent . handle_prompt (test_prompt ()). await ? ;
124- assert_eq! (response . role, Role :: Assistant );
125- }
126- }
250+ // Sessions
251+ NewSessionRequest , NewSessionResponse ,
252+ LoadSessionRequest , LoadSessionResponse ,
253+ SessionId ,
254+
255+ // Prompts
256+ PromptRequest , PromptResponse ,
257+ ContentBlock , ContentChunk ,
258+ StopReason ,
259+
260+ // Notifications
261+ SessionNotification , SessionUpdate ,
262+
263+ // MCP
264+ McpServer ,
265+ };
127266```
128267
129- ## Standard ACP Implementation
130-
131- Remember: An agent built with ` sacp ` is a standard ACP agent. It will work:
132-
133- - Directly with ACP editors (Zed, Claude Code, etc.)
134- - As the final component in a SACP proxy chain
135- - With any ACP-compatible tooling
268+ ## Complete Example
136269
137- The ` sacp ` crate just provides convenient Rust types and infrastructure .
270+ See the [ elizacp ] ( https://github.com/symposium-dev/symposium-acp/tree/main/src/elizacp ) crate for a complete working agent implementation with session management and MCP tool support .
138271
139272## Next Steps
140273
141274- See [ Protocol Reference] ( ./protocol.md ) for message format details
142- - Read the ` sacp ` crate documentation for API details
275+ - Read the ` sacp ` crate rustdoc for full API documentation
143276- Check the [ ACP specification] ( https://agentclientprotocol.com/ ) for protocol details
0 commit comments