sacp/lib.rs
1#![deny(missing_docs)]
2
3//! # sacp -- the Symposium Agent Client Protocol (ACP) SDK
4//!
5//! **sacp** is a Rust SDK for building agents and editors using the [Agent-Client Protocol (ACP)](https://agentclientprotocol.com/).
6//! It makes it easy to build ACP editors and clients -- or, indeed, any JSON-RPC-based application.
7//!
8//! ## Quick Start
9//!
10//! Building an ACP agent is straightforward with sacp's type-safe API:
11//!
12//! ```no_run
13//! use sacp::role::UntypedRole;
14//! use sacp::{MessageCx, UntypedMessage};
15//! use sacp::schema::{InitializeRequest, InitializeResponse, AgentCapabilities};
16//! use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
17//!
18//! # #[tokio::main]
19//! # async fn main() -> Result<(), sacp::Error> {
20//! // Start by creating an agent connection
21//! UntypedRole::builder()
22//! .name("my-agent") // Give it a name for logging purposes
23//! .on_receive_request(async move |initialize: InitializeRequest, request_cx, _cx| {
24//! // Create one or more request handlers -- these are attempted in order.
25//! // You can do anything you want in here, but you should eventually
26//! // respond to the request with `request_cx.respond(...)`:
27//! request_cx.respond(InitializeResponse {
28//! protocol_version: initialize.protocol_version,
29//! agent_capabilities: AgentCapabilities::default(),
30//! auth_methods: Default::default(),
31//! agent_info: Default::default(),
32//! meta: Default::default(),
33//! })
34//! })
35//! .on_receive_message(async move |message: MessageCx, cx| {
36//! // You can also handle any kind of message:
37//! message.respond_with_error(sacp::util::internal_error("TODO"), cx)
38//! })
39//! .serve(sacp::ByteStreams::new(
40//! tokio::io::stdout().compat_write(),
41//! tokio::io::stdin().compat(),
42//! ))
43//! .await
44//! # }
45//! ```
46//!
47//! ## Common Patterns
48//!
49//! ### Pattern 1: Defining Reusable Components
50//!
51//! When building agents or proxies, define a struct that implements [`Component`]. Internally, use the role's `builder()` method to set up handlers:
52//!
53//! ```rust,ignore
54//! use sacp::Component;
55//!
56//! struct MyAgent {
57//! config: AgentConfig,
58//! }
59//!
60//! impl Component for MyAgent {
61//! async fn serve(self, client: impl Component) -> Result<(), sacp::Error> {
62//! UntypedRole::builder()
63//! .name("my-agent")
64//! .on_receive_request(async move |req: PromptRequest, cx| {
65//! // Don't block the message loop! Use await_when_* for async work
66//! cx.respond(self.process_prompt(req))
67//! .await_when_result_received(async move |response| {
68//! // This runs after the response is received
69//! log_response(&response);
70//! cx.respond(response)
71//! })
72//! })
73//! .serve(client)
74//! .await
75//! }
76//! }
77//! ```
78//!
79//! **Important:** Message handlers run on the event loop. Blocking in a handler will prevent the connection from processing new messages.
80//! Use [`JrConnectionCx::spawn`] to offload expensive work, or use the `await_when_*` methods to avoid blocking.
81//!
82//! ### Pattern 2: Custom Message Handlers
83//!
84//! For reusable message handling logic, implement [`JrMessageHandler`] and use [`MatchMessage`](crate::util::MatchMessage) for dispatching:
85//!
86//! ```rust,ignore
87//! use sacp::{JrMessageHandler, MessageAndCx, Handled};
88//! use sacp::util::MatchMessage;
89//!
90//! struct MyHandler {
91//! state: Arc<Mutex<State>>,
92//! }
93//!
94//! impl JrMessageHandler for MyHandler {
95//! async fn handle_message(&mut self, message: MessageAndCx)
96//! -> Result<Handled<MessageAndCx>, sacp::Error>
97//! {
98//! MatchMessage::new(message)
99//! .if_request(async |req: MyRequest, cx| {
100//! // Handle using self.state
101//! cx.respond(MyResponse { /* ... */ })
102//! })
103//! .await
104//! .done()
105//! }
106//!
107//! fn describe_chain(&self) -> impl std::fmt::Debug { "MyHandler" }
108//! }
109//! ```
110//!
111//! ### Pattern 3: Connecting as a Client
112//!
113//! To connect to a JSON-RPC server and send requests, use `with_client`. Note the use of `async` (not `async move`)
114//! to share access to local variables:
115//!
116//! ```rust,ignore
117//! UntypedRole::builder()
118//! .on_receive_notification(async |notif: SessionUpdate, cx| {
119//! // Handle notifications from the server
120//! Ok(())
121//! })
122//! .with_client(sacp::ByteStreams::new(stdout, stdin), async |cx| {
123//! // Send requests using the connection context
124//! let response = cx.send_request(MyRequest { /* ... */ })
125//! .block_task()
126//! .await?;
127//!
128//! // Can access local variables here
129//! process_response(response);
130//!
131//! Ok(())
132//! })
133//! .await
134//! ```
135//!
136//! ## Using the Request Context
137//!
138//! The request context ([`JrRequestCx`]) provided to handlers is not just for responding—it offers several capabilities:
139//!
140//! - **Respond to the request:** `cx.respond(response)` sends a response back to the caller
141//! - **Send requests to the other side:** `cx.send_request(request)` initiates a new request
142//! - **Send notifications:** `cx.send_notification(notification)` sends a fire-and-forget message
143//! - **Spawn background tasks:** `cx.spawn(future)` runs work concurrently without blocking the message loop
144//!
145//! If you need a connection context that's independent of a particular request (for example, to store in a struct for later use),
146//! use `cx.connection_cx()`. This gives you a [`JrConnectionCx`] that can spawn tasks and send messages but isn't tied to
147//! responding to a specific request.
148//!
149//! ## Learning more
150//!
151//! You can learn more in the [docs for `JrConnection`](crate::JrConnection) or on our
152//! [GitHub Pages](https://github.com/symposium-dev/symposium-acp) site.
153//!
154//! You may also enjoy looking at some of these examples:
155//!
156//! - **[`simple_agent.rs`](https://github.com/symposium-dev/symposium-acp/blob/main/src/sacp/examples/simple_agent.rs)** - Minimal agent implementation
157//! - **[`yolo_one_shot_client.rs`](https://github.com/symposium-dev/symposium-acp/blob/main/src/sacp/examples/yolo_one_shot_client.rs)** - Complete client that spawns an agent and sends a prompt
158//! - **[`elizacp`](https://crates.io/crates/elizacp)** - Full working agent with session management (also useful for testing)
159//! - **[`sacp-conductor`](https://crates.io/crates/sacp-conductor)** - The "conductor" is an ACP agent that composes proxy components with a final agent.
160//!
161//! ## Related Crates
162//!
163//! - **[sacp-proxy](https://crates.io/crates/sacp-proxy)** - Framework for building ACP proxies that extend agent behavior
164//! - **[sacp-tokio](https://crates.io/crates/sacp-tokio)** - Tokio-specific utilities (process spawning, connection management)
165//! - **[sacp-conductor](https://crates.io/crates/sacp-conductor)** - Binary for orchestrating proxy chains
166
167/// Capability management for the `_meta.symposium` object
168mod capabilities;
169/// Component abstraction for agents and proxies
170pub mod component;
171/// JSON-RPC handler types for building custom message handlers
172pub mod handler;
173/// JSON-RPC connection and handler infrastructure
174mod jsonrpc;
175/// MCP declarations (minimal)
176pub mod mcp;
177/// MCP server support for providing MCP tools over ACP
178pub mod mcp_server;
179/// Proxy support for building ACP proxy components
180/// Role types for JSON-RPC connections
181pub mod role;
182/// ACP protocol schema types - all message types, requests, responses, and supporting types
183pub mod schema;
184/// Utility functions and types
185pub mod util;
186
187pub use capabilities::*;
188
189/// JSON-RPC message types.
190///
191/// This module re-exports types from the `jsonrpcmsg` crate that are transitively
192/// reachable through the public API (e.g., via [`Channel`]).
193///
194/// Users of the `sacp` crate can use these types without adding a direct dependency
195/// on `jsonrpcmsg`.
196pub mod jsonrpcmsg {
197 pub use jsonrpcmsg::{Error, Id, Message, Params, Request, Response};
198}
199
200pub use jsonrpc::{
201 ByteStreams, Channel, Handled, IntoHandled, JrConnection, JrConnectionBuilder, JrConnectionCx,
202 JrMessage, JrMessageHandler, JrMessageHandlerSend, JrNotification, JrRequest, JrRequestCx,
203 JrResponse, JrResponsePayload, Lines, MessageCx, NullHandler, UntypedMessage,
204};
205
206pub use role::{
207 Agent, AgentToClient, Client, ClientToAgent, Conductor, HasDefaultEndpoint, HasEndpoint,
208 JrEndpoint, JrRole, ProxyToConductor,
209};
210
211pub use component::{Component, DynComponent};
212
213// Re-export BoxFuture for implementing Component traits
214pub use futures::future::BoxFuture;
215
216// Re-export the six primary message enum types at the root
217pub use schema::{
218 AgentNotification, AgentRequest, AgentResponse, ClientNotification, ClientRequest,
219 ClientResponse,
220};
221
222// Re-export commonly used infrastructure types for convenience
223pub use schema::{Error, ErrorCode};
224
225// Re-export derive macros for custom JSON-RPC types
226pub use sacp_derive::{JrNotification, JrRequest, JrResponsePayload};