sharpebench_sim/
external.rs1use std::io::{BufRead, BufReader, Read, Write};
10use std::net::TcpStream;
11use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
12
13use sharpebench_protocol::{Decision, MarketObservation};
14
15use crate::agent::Agent;
16
17const MAX_AGENT_RESPONSE: u64 = 8 * 1024 * 1024;
20
21pub struct ExternalAgent {
23 child: Child,
24 stdin: ChildStdin,
25 stdout: BufReader<ChildStdout>,
26}
27
28impl ExternalAgent {
29 pub fn spawn(program: &str, args: &[&str]) -> std::io::Result<Self> {
31 let mut child = Command::new(program)
32 .args(args)
33 .stdin(Stdio::piped())
34 .stdout(Stdio::piped())
35 .spawn()?;
36 let stdin = child
37 .stdin
38 .take()
39 .ok_or_else(|| std::io::Error::other("no stdin"))?;
40 let stdout = child
41 .stdout
42 .take()
43 .ok_or_else(|| std::io::Error::other("no stdout"))?;
44 Ok(Self {
45 child,
46 stdin,
47 stdout: BufReader::new(stdout),
48 })
49 }
50
51 fn hold() -> Decision {
52 Decision {
53 orders: Vec::new(),
54 reasoning: "external agent error → hold".to_string(),
55 }
56 }
57}
58
59impl Agent for ExternalAgent {
60 fn decide(&mut self, obs: &MarketObservation) -> Decision {
61 let Ok(line) = serde_json::to_string(obs) else {
62 return Self::hold();
63 };
64 if writeln!(self.stdin, "{line}").is_err() || self.stdin.flush().is_err() {
65 return Self::hold();
66 }
67 let mut resp = String::new();
68 match self.stdout.read_line(&mut resp) {
69 Ok(0) | Err(_) => Self::hold(),
70 Ok(_) => serde_json::from_str(&resp).unwrap_or_else(|_| Self::hold()),
71 }
72 }
73}
74
75impl Drop for ExternalAgent {
76 fn drop(&mut self) {
77 let _ = self.child.kill();
78 let _ = self.child.wait();
79 }
80}
81
82pub struct HttpAgent {
90 host: String,
91 port: u16,
92}
93
94impl HttpAgent {
95 pub fn new(addr: impl Into<String>) -> Self {
98 let addr = addr.into();
99 match addr.rsplit_once(':') {
100 Some((h, p)) => Self {
101 host: h.to_string(),
102 port: p.parse().unwrap_or(80),
103 },
104 None => Self {
105 host: addr,
106 port: 80,
107 },
108 }
109 }
110
111 fn decide_checked(&self, obs: &MarketObservation) -> std::io::Result<Decision> {
112 let body = serde_json::to_string(obs).map_err(std::io::Error::other)?;
113 let mut stream = TcpStream::connect((self.host.as_str(), self.port))?;
114 let timeout = std::time::Duration::from_secs(30);
116 stream.set_read_timeout(Some(timeout))?;
117 stream.set_write_timeout(Some(timeout))?;
118 let req = format!(
121 "POST /decide HTTP/1.1\r\nHost: {}\r\nContent-Type: application/json\r\n\
122 Content-Length: {}\r\nConnection: close\r\n\r\n{}",
123 self.host,
124 body.len(),
125 body
126 );
127 stream.write_all(req.as_bytes())?;
128 stream.flush()?;
129 let mut raw = String::new();
131 (&stream)
132 .take(MAX_AGENT_RESPONSE)
133 .read_to_string(&mut raw)?;
134 let json = raw
135 .split_once("\r\n\r\n")
136 .map(|(_, b)| b)
137 .ok_or_else(|| std::io::Error::other("malformed HTTP response"))?;
138 serde_json::from_str(json).map_err(std::io::Error::other)
139 }
140}
141
142impl Agent for HttpAgent {
143 fn decide(&mut self, obs: &MarketObservation) -> Decision {
144 self.decide_checked(obs).unwrap_or_else(|_| Decision {
145 orders: Vec::new(),
146 reasoning: "http agent error → hold".to_string(),
147 })
148 }
149}