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 ;
2315use sacp:: JrConnection ;
2416use 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 } ;
3021use 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]
3473async 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}
0 commit comments