Skip to content

Commit 3481d89

Browse files
nikomatsakisclaude
andcommitted
refactor(sacp): remove circular dependency with sacp-tokio
Inline the AcpAgent and JrConnectionExt functionality directly into the yolo_one_shot_client example to break the circular dependency between sacp and sacp-tokio. The example now uses shell-words directly for command parsing and spawns the agent process manually. The example includes a comment pointing users to sacp_tokio for a simpler API when building their own clients. Co-authored-by: Claude <claude@anthropic.com>
1 parent 310ec56 commit 3481d89

File tree

3 files changed

+93
-10
lines changed

3 files changed

+93
-10
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/sacp/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ uuid.workspace = true
2222

2323
[dev-dependencies]
2424
expect-test.workspace = true
25-
sacp-tokio = { version = "1.0.0-alpha", path = "../sacp-tokio" }
26-
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "io-util", "time"] }
25+
shell-words = "1.1"
26+
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "io-util", "time", "process"] }
2727
tokio-util.workspace = true
2828
sacp-test = { path = "../sacp-test" }

src/sacp/examples/yolo_one_shot_client.rs

Lines changed: 90 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,86 @@
2222
2323
use sacp::JrConnection;
2424
use sacp::schema::{
25-
ContentBlock, InitializeRequest, NewSessionRequest, PromptRequest, RequestPermissionOutcome,
26-
RequestPermissionRequest, RequestPermissionResponse, SessionNotification, TextContent,
27-
VERSION as PROTOCOL_VERSION,
25+
ContentBlock, InitializeRequest, McpServer, NewSessionRequest, PromptRequest,
26+
RequestPermissionOutcome, RequestPermissionRequest, RequestPermissionResponse,
27+
SessionNotification, TextContent, VERSION as PROTOCOL_VERSION,
2828
};
29-
use sacp_tokio::{AcpAgent, JrConnectionExt};
3029
use std::path::PathBuf;
31-
use std::str::FromStr;
30+
use tokio::process::Child;
31+
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
32+
33+
// NOTE: This code is inlined here to avoid a circular dependency between `sacp` and `sacp-tokio`.
34+
// If you're writing your own client, you can use `sacp_tokio::AcpAgent` and `JrConnectionExt::to_agent()`
35+
// to simplify this setup significantly.
36+
37+
/// Parse agent configuration from either a command string or JSON.
38+
fn parse_agent_config(s: &str) -> Result<McpServer, Box<dyn std::error::Error>> {
39+
let trimmed = s.trim();
40+
41+
// If it starts with '{', try to parse as JSON
42+
if trimmed.starts_with('{') {
43+
let server: McpServer = serde_json::from_str(trimmed)?;
44+
return Ok(server);
45+
}
46+
47+
// Otherwise, parse as a command string
48+
let parts = shell_words::split(trimmed)?;
49+
if parts.is_empty() {
50+
return Err("Command string cannot be empty".into());
51+
}
52+
53+
let command = PathBuf::from(&parts[0]);
54+
let args = parts[1..].to_vec();
55+
let name = command
56+
.file_name()
57+
.and_then(|n| n.to_str())
58+
.unwrap_or("agent")
59+
.to_string();
60+
61+
Ok(McpServer::Stdio {
62+
name,
63+
command,
64+
args,
65+
env: vec![],
66+
})
67+
}
68+
69+
/// Spawn a process for the agent and get stdio streams.
70+
fn spawn_agent_process(
71+
server: &McpServer,
72+
) -> Result<
73+
(
74+
tokio::process::ChildStdin,
75+
tokio::process::ChildStdout,
76+
Child,
77+
),
78+
Box<dyn std::error::Error>,
79+
> {
80+
match server {
81+
McpServer::Stdio {
82+
command,
83+
args,
84+
env,
85+
name: _,
86+
} => {
87+
let mut cmd = tokio::process::Command::new(command);
88+
cmd.args(args);
89+
for env_var in env {
90+
cmd.env(&env_var.name, &env_var.value);
91+
}
92+
cmd.stdin(std::process::Stdio::piped())
93+
.stdout(std::process::Stdio::piped());
94+
95+
let mut child = cmd.spawn()?;
96+
let child_stdin = child.stdin.take().ok_or("Failed to open stdin")?;
97+
let child_stdout = child.stdout.take().ok_or("Failed to open stdout")?;
98+
99+
Ok((child_stdin, child_stdout, child))
100+
}
101+
McpServer::Http { .. } => Err("HTTP transport not yet supported".into()),
102+
McpServer::Sse { .. } => Err("SSE transport not yet supported".into()),
103+
}
104+
}
32105

33106
#[tokio::main]
34107
async fn main() -> Result<(), Box<dyn std::error::Error>> {
@@ -53,12 +126,19 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
53126
let agent_config = &args[2];
54127

55128
// Parse the agent configuration
56-
let agent = AcpAgent::from_str(agent_config)?;
129+
let server = parse_agent_config(agent_config)?;
57130

58131
eprintln!("🚀 Spawning agent and connecting...");
59132

133+
// Spawn the agent process
134+
let (child_stdin, child_stdout, mut child) = spawn_agent_process(&server)?;
135+
136+
// Create a JrConnection with the agent's stdio streams
137+
// NOTE: Using sacp_tokio::JrConnectionExt::to_agent() would simplify this setup
138+
let connection = JrConnection::new(child_stdin.compat_write(), child_stdout.compat());
139+
60140
// Run the client
61-
JrConnection::to_agent(agent)?
141+
connection
62142
.on_receive_notification(async move |notification: SessionNotification, _cx| {
63143
// Print session updates to stdout (so 2>/dev/null shows only agent output)
64144
println!("{:?}", notification.update);
@@ -133,5 +213,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
133213
})
134214
.await?;
135215

216+
// Kill the child process when done
217+
let _ = child.kill().await;
218+
136219
Ok(())
137220
}

0 commit comments

Comments
 (0)