Skip to content

Commit 68e1c87

Browse files
authored
Merge pull request #12 from nikomatsakis/main
You only prompt once!
2 parents 310ec56 + f5a23f7 commit 68e1c87

File tree

6 files changed

+234
-44
lines changed

6 files changed

+234
-44
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ members = [
55
"src/sacp",
66
"src/sacp-tokio",
77
"src/elizacp",
8-
"src/sacp-test",
8+
"src/sacp-test", "src/yopo",
99
]
1010
resolver = "2"
1111

src/sacp/Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ tracing.workspace = true
2121
uuid.workspace = true
2222

2323
[dev-dependencies]
24+
clap = { workspace = true, features = ["derive"] }
2425
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"] }
26+
shell-words = "1.1"
27+
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "io-util", "time", "process"] }
2728
tokio-util.workspace = true
2829
sacp-test = { path = "../sacp-test" }

src/sacp/examples/yolo_one_shot_client.rs

Lines changed: 70 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,91 @@
11
//! YOLO one-shot client: A simple ACP client that runs a single prompt against an agent.
22
//!
3-
//! This client:
4-
//! - Takes a prompt and agent configuration as arguments
5-
//! - Spawns the agent
6-
//! - Sends the prompt
7-
//! - Auto-approves all permission requests
8-
//! - Prints all session updates to stdout
9-
//! - Runs until the agent completes
3+
//! This is a simplified example showing basic ACP client usage. It only supports
4+
//! simple command strings (not JSON configs or environment variables).
105
//!
11-
//! # Usage
6+
//! For a more full-featured client with JSON config support, see the `yopo` binary crate.
127
//!
13-
//! With a command:
14-
//! ```bash
15-
//! cargo run --example yolo_one_shot_client -- "What is 2+2?" "python my_agent.py"
16-
//! ```
8+
//! # Usage
179
//!
18-
//! With JSON config:
1910
//! ```bash
20-
//! cargo run --example yolo_one_shot_client -- "Hello!" '{"type":"stdio","name":"my-agent","command":"python","args":["agent.py"],"env":[]}'
11+
//! cargo run --example yolo_one_shot_client -- --command "python my_agent.py" "What is 2+2?"
2112
//! ```
2213
14+
use clap::Parser;
2315
use sacp::JrConnection;
2416
use sacp::schema::{
2517
ContentBlock, InitializeRequest, NewSessionRequest, PromptRequest, RequestPermissionOutcome,
2618
RequestPermissionRequest, RequestPermissionResponse, SessionNotification, TextContent,
2719
VERSION as PROTOCOL_VERSION,
2820
};
29-
use sacp_tokio::{AcpAgent, JrConnectionExt};
3021
use std::path::PathBuf;
31-
use std::str::FromStr;
22+
use tokio::process::Child;
23+
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
24+
25+
#[derive(Parser)]
26+
#[command(name = "yolo-one-shot-client")]
27+
#[command(about = "A simple ACP client for one-shot prompts", long_about = None)]
28+
struct Cli {
29+
/// The command to run the agent (e.g., "python my_agent.py")
30+
#[arg(short, long)]
31+
command: String,
32+
33+
/// The prompt to send to the agent
34+
prompt: String,
35+
}
36+
37+
/// Parse a command string into command and args
38+
fn parse_command_string(s: &str) -> Result<(PathBuf, Vec<String>), Box<dyn std::error::Error>> {
39+
let parts = shell_words::split(s)?;
40+
if parts.is_empty() {
41+
return Err("Command string cannot be empty".into());
42+
}
43+
let command = PathBuf::from(&parts[0]);
44+
let args = parts[1..].to_vec();
45+
Ok((command, args))
46+
}
47+
48+
/// Spawn a process for the agent and get stdio streams.
49+
fn spawn_agent_process(
50+
command: PathBuf,
51+
args: Vec<String>,
52+
) -> Result<
53+
(
54+
tokio::process::ChildStdin,
55+
tokio::process::ChildStdout,
56+
Child,
57+
),
58+
Box<dyn std::error::Error>,
59+
> {
60+
let mut cmd = tokio::process::Command::new(&command);
61+
cmd.args(&args);
62+
cmd.stdin(std::process::Stdio::piped())
63+
.stdout(std::process::Stdio::piped());
64+
65+
let mut child = cmd.spawn()?;
66+
let child_stdin = child.stdin.take().ok_or("Failed to open stdin")?;
67+
let child_stdout = child.stdout.take().ok_or("Failed to open stdout")?;
68+
69+
Ok((child_stdin, child_stdout, child))
70+
}
3271

3372
#[tokio::main]
3473
async fn main() -> Result<(), Box<dyn std::error::Error>> {
35-
// Parse command line arguments
36-
let args: Vec<String> = std::env::args().collect();
37-
if args.len() != 3 {
38-
eprintln!("Usage: {} <prompt> <agent-config>", args[0]);
39-
eprintln!();
40-
eprintln!(" <prompt> - The prompt to send to the agent");
41-
eprintln!(" <agent-config> - Either a command string or JSON (starting with '{{')");
42-
eprintln!();
43-
eprintln!("Examples:");
44-
eprintln!(" {} \"What is 2+2?\" \"python my_agent.py\"", args[0]);
45-
eprintln!(
46-
" {} \"Hello!\" '{{\"type\":\"stdio\",\"name\":\"agent\",\"command\":\"python\",\"args\":[\"agent.py\"],\"env\":[]}}'",
47-
args[0]
48-
);
49-
std::process::exit(1);
50-
}
74+
let cli = Cli::parse();
5175

52-
let prompt = &args[1];
53-
let agent_config = &args[2];
76+
// Parse the command string
77+
let (command, args) = parse_command_string(&cli.command)?;
5478

55-
// Parse the agent configuration
56-
let agent = AcpAgent::from_str(agent_config)?;
79+
eprintln!("🚀 Spawning agent: {} {:?}", command.display(), args);
5780

58-
eprintln!("🚀 Spawning agent and connecting...");
81+
// Spawn the agent process
82+
let (child_stdin, child_stdout, mut child) = spawn_agent_process(command, args)?;
83+
84+
// Create a JrConnection with the agent's stdio streams
85+
let connection = JrConnection::new(child_stdin.compat_write(), child_stdout.compat());
5986

6087
// Run the client
61-
JrConnection::to_agent(agent)?
88+
connection
6289
.on_receive_notification(async move |notification: SessionNotification, _cx| {
6390
// Print session updates to stdout (so 2>/dev/null shows only agent output)
6491
println!("{:?}", notification.update);
@@ -112,12 +139,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
112139
eprintln!("✓ Session created: {}", session_id);
113140

114141
// Send the prompt
115-
eprintln!("💬 Sending prompt: \"{}\"", prompt);
142+
eprintln!("💬 Sending prompt: \"{}\"", cli.prompt);
116143
let prompt_response = cx
117144
.send_request(PromptRequest {
118145
session_id: session_id.clone(),
119146
prompt: vec![ContentBlock::Text(TextContent {
120-
text: prompt.to_string(),
147+
text: cli.prompt.clone(),
121148
annotations: None,
122149
meta: None,
123150
})],
@@ -133,5 +160,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
133160
})
134161
.await?;
135162

163+
// Kill the child process when done
164+
let _ = child.kill().await;
165+
136166
Ok(())
137167
}

src/yopo/Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "yopo"
3+
version = "1.0.0-alpha"
4+
edition = "2024"
5+
description = "YOPO (You Only Prompt Once) - A simple ACP client for one-shot prompts"
6+
license = "MIT OR Apache-2.0"
7+
repository = "https://github.com/symposium-dev/symposium-acp"
8+
9+
[dependencies]
10+
sacp = { version = "1.0.0-alpha", path = "../sacp" }
11+
sacp-tokio = { version = "1.0.0-alpha", path = "../sacp-tokio" }
12+
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }

src/yopo/src/main.rs

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
//! YOPO (You Only Prompt Once) - A simple ACP client for one-shot prompts
2+
//!
3+
//! This client:
4+
//! - Takes a prompt and agent configuration as arguments
5+
//! - Spawns the agent
6+
//! - Sends the prompt
7+
//! - Auto-approves all permission requests
8+
//! - Prints all session updates to stdout
9+
//! - Runs until the agent completes
10+
//!
11+
//! # Usage
12+
//!
13+
//! With a command:
14+
//! ```bash
15+
//! yopo "What is 2+2?" "python my_agent.py"
16+
//! ```
17+
//!
18+
//! With JSON config:
19+
//! ```bash
20+
//! yopo "Hello!" '{"type":"stdio","name":"my-agent","command":"python","args":["agent.py"],"env":[]}'
21+
//! ```
22+
23+
use sacp::JrConnection;
24+
use sacp::schema::{
25+
ContentBlock, InitializeRequest, NewSessionRequest, PromptRequest, RequestPermissionOutcome,
26+
RequestPermissionRequest, RequestPermissionResponse, SessionNotification, TextContent,
27+
VERSION as PROTOCOL_VERSION,
28+
};
29+
use sacp_tokio::{AcpAgent, JrConnectionExt};
30+
use std::path::PathBuf;
31+
use std::str::FromStr;
32+
33+
#[tokio::main]
34+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
35+
// Parse command line arguments
36+
let args: Vec<String> = std::env::args().collect();
37+
if args.len() != 3 {
38+
eprintln!("Usage: {} <prompt> <agent-config>", args[0]);
39+
eprintln!();
40+
eprintln!(" <prompt> - The prompt to send to the agent");
41+
eprintln!(" <agent-config> - Either a command string or JSON (starting with '{{')");
42+
eprintln!();
43+
eprintln!("Examples:");
44+
eprintln!(" {} \"What is 2+2?\" \"python my_agent.py\"", args[0]);
45+
eprintln!(
46+
" {} \"Hello!\" '{{\"type\":\"stdio\",\"name\":\"agent\",\"command\":\"python\",\"args\":[\"agent.py\"],\"env\":[]}}'",
47+
args[0]
48+
);
49+
std::process::exit(1);
50+
}
51+
52+
let prompt = &args[1];
53+
let agent_config = &args[2];
54+
55+
// Parse the agent configuration
56+
let agent = AcpAgent::from_str(agent_config)?;
57+
58+
eprintln!("🚀 Spawning agent and connecting...");
59+
60+
// Run the client
61+
JrConnection::to_agent(agent)?
62+
.on_receive_notification(async move |notification: SessionNotification, _cx| {
63+
// Print session updates to stdout (so 2>/dev/null shows only agent output)
64+
println!("{:?}", notification.update);
65+
Ok(())
66+
})
67+
.on_receive_request(async move |request: RequestPermissionRequest, request_cx| {
68+
// YOPO: Auto-approve all permission requests by selecting the first option
69+
eprintln!("✅ Auto-approving permission request: {:?}", request);
70+
let option_id = request.options.first().map(|opt| opt.id.clone());
71+
match option_id {
72+
Some(id) => request_cx.respond(RequestPermissionResponse {
73+
outcome: RequestPermissionOutcome::Selected { option_id: id },
74+
meta: None,
75+
}),
76+
None => {
77+
eprintln!("⚠️ No options provided in permission request, cancelling");
78+
request_cx.respond(RequestPermissionResponse {
79+
outcome: RequestPermissionOutcome::Cancelled,
80+
meta: None,
81+
})
82+
}
83+
}
84+
})
85+
.with_client(|cx: sacp::JrConnectionCx| async move {
86+
// Initialize the agent
87+
eprintln!("🤝 Initializing agent...");
88+
let init_response = cx
89+
.send_request(InitializeRequest {
90+
protocol_version: PROTOCOL_VERSION,
91+
client_capabilities: Default::default(),
92+
client_info: Default::default(),
93+
meta: None,
94+
})
95+
.block_task()
96+
.await?;
97+
98+
eprintln!("✓ Agent initialized: {:?}", init_response.agent_info);
99+
100+
// Create a new session
101+
eprintln!("📝 Creating new session...");
102+
let new_session_response = cx
103+
.send_request(NewSessionRequest {
104+
mcp_servers: vec![],
105+
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
106+
meta: None,
107+
})
108+
.block_task()
109+
.await?;
110+
111+
let session_id = new_session_response.session_id;
112+
eprintln!("✓ Session created: {}", session_id);
113+
114+
// Send the prompt
115+
eprintln!("💬 Sending prompt: \"{}\"", prompt);
116+
let prompt_response = cx
117+
.send_request(PromptRequest {
118+
session_id: session_id.clone(),
119+
prompt: vec![ContentBlock::Text(TextContent {
120+
text: prompt.to_string(),
121+
annotations: None,
122+
meta: None,
123+
})],
124+
meta: None,
125+
})
126+
.block_task()
127+
.await?;
128+
129+
eprintln!("✅ Agent completed!");
130+
eprintln!("Stop reason: {:?}", prompt_response.stop_reason);
131+
132+
Ok(())
133+
})
134+
.await?;
135+
136+
Ok(())
137+
}

0 commit comments

Comments
 (0)